summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/controls/IconButton.qml27
-rw-r--r--components/controls/IconTextButton.qml15
-rw-r--r--components/controls/SplitButton.qml15
-rw-r--r--components/controls/TextButton.qml15
-rw-r--r--modules/utilities/RecordingDeleteModal.qml8
-rw-r--r--modules/utilities/cards/Record.qml317
-rw-r--r--modules/utilities/cards/RecordingList.qml237
-rw-r--r--services/Recorder.qml60
8 files changed, 453 insertions, 241 deletions
diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml
index 0b39d97..6e55064 100644
--- a/components/controls/IconButton.qml
+++ b/components/controls/IconButton.qml
@@ -15,7 +15,7 @@ StyledRect {
property alias icon: label.text
property bool checked
property bool toggle
- property real padding: type == IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller
+ property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller
property alias font: label.font
property int type: IconButton.Filled
@@ -23,18 +23,25 @@ StyledRect {
property alias label: label
property bool internalChecked
- property color activeColour: type == IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
- property color inactiveColour: type == IconButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
- property color activeOnColour: type == IconButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
- property color inactiveOnColour: type == IconButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
-
- function onClicked(): void {
+ property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
+ property color inactiveColour: {
+ if (!toggle && type === IconButton.Filled)
+ return Colours.palette.m3primary;
+ return type === IconButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer;
+ }
+ property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
+ property color inactiveOnColour: {
+ if (!toggle && type === IconButton.Filled)
+ return Colours.palette.m3onPrimary;
+ return type === IconButton.Filled ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSecondaryContainer;
}
+ signal clicked
+
onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2
- color: type == IconButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
+ color: type === IconButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
implicitWidth: implicitHeight
implicitHeight: label.implicitHeight + padding * 2
@@ -47,7 +54,7 @@ StyledRect {
function onClicked(): void {
if (root.toggle)
root.internalChecked = !root.internalChecked;
- root.onClicked();
+ root.clicked();
}
}
@@ -56,7 +63,7 @@ StyledRect {
anchors.centerIn: parent
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
- fill: root.internalChecked ? 1 : 0
+ fill: !root.toggle || root.internalChecked ? 1 : 0
Behavior on fill {
Anim {}
diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml
index ba60f3b..c776401 100644
--- a/components/controls/IconTextButton.qml
+++ b/components/controls/IconTextButton.qml
@@ -26,18 +26,17 @@ StyledRect {
property alias label: label
property bool internalChecked
- property color activeColour: type == IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
- property color inactiveColour: type == IconTextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
- property color activeOnColour: type == IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
- property color inactiveOnColour: type == IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
+ property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
+ property color inactiveColour: type === IconTextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
+ property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
+ property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
- function onClicked(): void {
- }
+ signal clicked
onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2
- color: type == IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
+ color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
@@ -50,7 +49,7 @@ StyledRect {
function onClicked(): void {
if (root.toggle)
root.internalChecked = !root.internalChecked;
- root.onClicked();
+ root.clicked();
}
}
diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml
index 6c92285..a61a536 100644
--- a/components/controls/SplitButton.qml
+++ b/components/controls/SplitButton.qml
@@ -15,6 +15,7 @@ Row {
property real horizontalPadding: Appearance.padding.normal
property real verticalPadding: Appearance.padding.smaller
property int type: SplitButton.Filled
+ property bool disabled
property alias menuItems: menu.items
property alias active: menu.active
property alias expanded: menu.expanded
@@ -22,6 +23,8 @@ Row {
property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
+ readonly property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1)
+ readonly property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38)
spacing: Math.floor(Appearance.spacing.small / 2)
@@ -29,7 +32,7 @@ Row {
radius: implicitHeight / 2
topRightRadius: Appearance.rounding.small / 2
bottomRightRadius: Appearance.rounding.small / 2
- color: root.colour
+ color: root.disabled ? root.disabledColour : root.colour
implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
implicitHeight: expandBtn.implicitHeight
@@ -40,6 +43,7 @@ Row {
rect.topRightRadius: parent.topRightRadius
rect.bottomRightRadius: parent.bottomRightRadius
color: root.textColour
+ disabled: root.disabled
function onClicked(): void {
root.active?.clicked();
@@ -59,7 +63,7 @@ Row {
Layout.alignment: Qt.AlignVCenter
animate: true
text: root.active?.activeIcon ?? ""
- color: root.textColour
+ color: root.disabled ? root.disabledTextColour : root.textColour
fill: 1
}
@@ -70,7 +74,7 @@ Row {
Layout.preferredWidth: implicitWidth
animate: true
text: root.active?.activeText ?? ""
- color: root.textColour
+ color: root.disabled ? root.disabledTextColour : root.textColour
clip: true
Behavior on Layout.preferredWidth {
@@ -90,7 +94,7 @@ Row {
radius: implicitHeight / 2
topLeftRadius: rad
bottomLeftRadius: rad
- color: root.colour
+ color: root.disabled ? root.disabledColour : root.colour
implicitWidth: implicitHeight
implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2
@@ -101,6 +105,7 @@ Row {
rect.topRightRadius: parent.topRightRadius
rect.bottomRightRadius: parent.bottomRightRadius
color: root.textColour
+ disabled: root.disabled
function onClicked(): void {
root.expanded = !root.expanded;
@@ -114,7 +119,7 @@ Row {
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
text: "expand_more"
- color: root.textColour
+ color: root.disabled ? root.disabledTextColour : root.textColour
rotation: root.expanded ? 180 : 0
Behavior on anchors.horizontalCenterOffset {
diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml
index a4c8666..65e9f09 100644
--- a/components/controls/TextButton.qml
+++ b/components/controls/TextButton.qml
@@ -24,18 +24,17 @@ StyledRect {
property alias label: label
property bool internalChecked
- property color activeColour: type == TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
- property color inactiveColour: type == TextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
- property color activeOnColour: type == TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
- property color inactiveOnColour: type == TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
+ property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
+ property color inactiveColour: type === TextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
+ property color activeOnColour: type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
+ property color inactiveOnColour: type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
- function onClicked(): void {
- }
+ signal clicked
onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2
- color: type == TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
+ color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour
implicitWidth: label.implicitWidth + horizontalPadding * 2
implicitHeight: label.implicitHeight + verticalPadding * 2
@@ -48,7 +47,7 @@ StyledRect {
function onClicked(): void {
if (root.toggle)
root.internalChecked = !root.internalChecked;
- root.onClicked();
+ root.clicked();
}
}
diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml
index 55de690..83e6d59 100644
--- a/modules/utilities/RecordingDeleteModal.qml
+++ b/modules/utilities/RecordingDeleteModal.qml
@@ -181,10 +181,7 @@ Loader {
type: TextButton.Text
label.color: Colours.palette.m3primary
stateLayer.color: Colours.palette.m3primary
-
- function onClicked(): void {
- root.props.recordingConfirmDelete = "";
- }
+ onClicked: root.props.recordingConfirmDelete = ""
}
TextButton {
@@ -192,8 +189,7 @@ Loader {
type: TextButton.Text
label.color: Colours.palette.m3primary
stateLayer.color: Colours.palette.m3primary
-
- function onClicked(): void {
+ onClicked: {
CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete));
root.props.recordingConfirmDelete = "";
}
diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml
index fa45cf7..963b028 100644
--- a/modules/utilities/cards/Record.qml
+++ b/modules/utilities/cards/Record.qml
@@ -2,14 +2,8 @@ pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
-import qs.components.containers
import qs.services
import qs.config
-import qs.utils
-import Caelestia
-import Caelestia.Models
-import Quickshell
-import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
@@ -18,7 +12,6 @@ StyledRect {
required property var props
required property var visibilities
- property bool modePickerExpanded
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
@@ -80,6 +73,7 @@ StyledRect {
}
SplitButton {
+ disabled: Recorder.running
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
@@ -88,250 +82,181 @@ StyledRect {
icon: "fullscreen"
text: qsTr("Record fullscreen")
activeText: qsTr("Fullscreen")
- onClicked: Recorder.toggle()
+ onClicked: Recorder.start()
},
MenuItem {
icon: "screenshot_region"
text: qsTr("Record region")
activeText: qsTr("Region")
- onClicked: Recorder.toggle(["-r"])
+ onClicked: Recorder.start(["-r"])
},
MenuItem {
icon: "select_to_speak"
text: qsTr("Record fullscreen with sound")
activeText: qsTr("Fullscreen")
- onClicked: Recorder.toggle(["-s"])
+ onClicked: Recorder.start(["-s"])
},
MenuItem {
icon: "volume_up"
text: qsTr("Record region with sound")
activeText: qsTr("Region")
- onClicked: Recorder.toggle(["-sr"])
+ onClicked: Recorder.start(["-sr"])
}
]
}
}
- WrapperMouseArea {
- Layout.bottomMargin: -layout.spacing
- Layout.fillWidth: true
-
- cursorShape: Qt.PointingHandCursor
- onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
-
- RowLayout {
- spacing: Appearance.spacing.smaller
+ Loader {
+ id: listOrControls
- MaterialIcon {
- Layout.alignment: Qt.AlignVCenter
- text: "list"
- font.pointSize: Appearance.font.size.large
- }
+ property bool running: Recorder.running
- StyledText {
- Layout.alignment: Qt.AlignVCenter
- Layout.fillWidth: true
- text: qsTr("Recordings")
- font.pointSize: Appearance.font.size.normal
- }
+ Layout.fillWidth: true
+ Layout.preferredHeight: implicitHeight
+ sourceComponent: running ? recordingControls : recordingList
- IconButton {
- icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
- type: IconButton.Text
- label.animate: true
+ Behavior on Layout.preferredHeight {
+ Anim {}
+ }
- function onClicked(): void {
- root.props.recordingListExpanded = !root.props.recordingListExpanded;
+ Behavior on running {
+ SequentialAnimation {
+ ParallelAnimation {
+ Anim {
+ target: listOrControls
+ property: "scale"
+ to: 0.7
+ duration: Appearance.anim.durations.small
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
+ }
+ Anim {
+ target: listOrControls
+ property: "opacity"
+ to: 0
+ duration: Appearance.anim.durations.small
+ easing.bezierCurve: Appearance.anim.curves.standardAccel
+ }
+ }
+ PropertyAction {}
+ ParallelAnimation {
+ Anim {
+ target: listOrControls
+ property: "scale"
+ to: 1
+ duration: Appearance.anim.durations.small
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
+ }
+ Anim {
+ target: listOrControls
+ property: "opacity"
+ to: 1
+ duration: Appearance.anim.durations.small
+ easing.bezierCurve: Appearance.anim.curves.standardDecel
+ }
}
}
}
}
+ }
- StyledListView {
- id: recordingList
-
- model: FileSystemModel {
- path: Paths.recsdir
- nameFilters: ["recording_*.mp4"]
- sortReverse: true
- }
-
- Layout.fillWidth: true
- Layout.rightMargin: -Appearance.spacing.small
- implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
- clip: true
+ Component {
+ id: recordingList
- StyledScrollBar.vertical: StyledScrollBar {}
+ RecordingList {
+ props: root.props
+ visibilities: root.visibilities
+ }
+ }
- delegate: RowLayout {
- id: recording
+ Component {
+ id: recordingControls
- required property FileSystemEntry modelData
- property string baseName
+ RowLayout {
+ spacing: Appearance.spacing.normal
- anchors.left: recordingList.contentItem.left
- anchors.right: recordingList.contentItem.right
- anchors.rightMargin: Appearance.spacing.small
- spacing: Appearance.spacing.small / 2
+ StyledRect {
+ radius: Appearance.rounding.full
+ color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error
- Component.onCompleted: baseName = modelData.baseName
+ implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
+ implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
StyledText {
- Layout.fillWidth: true
- Layout.rightMargin: Appearance.spacing.small / 2
- text: {
- const time = recording.baseName;
- const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
- if (!matches)
- return time;
- const date = new Date(...matches.slice(1));
- return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
- }
- color: Colours.palette.m3onSurfaceVariant
- elide: Text.ElideRight
- }
+ id: recText
- IconButton {
- icon: "play_arrow"
- type: IconButton.Text
-
- function onClicked(): void {
- root.visibilities.utilities = false;
- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
- }
+ anchors.centerIn: parent
+ animate: true
+ text: Recorder.paused ? "PAUSED" : "REC"
+ color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError
+ font.family: Appearance.font.family.mono
}
- IconButton {
- icon: "folder"
- type: IconButton.Text
-
- function onClicked(): void {
- root.visibilities.utilities = false;
- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
- }
+ Behavior on implicitWidth {
+ Anim {}
}
- IconButton {
- icon: "delete_forever"
- type: IconButton.Text
- label.color: Colours.palette.m3error
- stateLayer.color: Colours.palette.m3error
+ SequentialAnimation on opacity {
+ running: !Recorder.paused
+ alwaysRunToEnd: true
+ loops: Animation.Infinite
- function onClicked(): void {
- root.props.recordingConfirmDelete = recording.modelData.path;
+ Anim {
+ from: 1
+ to: 0
+ duration: Appearance.anim.durations.large
+ easing.bezierCurve: Appearance.anim.curves.emphasizedAccel
+ }
+ Anim {
+ from: 0
+ to: 1
+ duration: Appearance.anim.durations.extraLarge
+ easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
}
- add: Transition {
- Anim {
- property: "opacity"
- from: 0
- to: 1
- }
- Anim {
- property: "scale"
- from: 0.5
- to: 1
- }
- }
+ StyledText {
+ text: {
+ const elapsed = Recorder.elapsed;
- remove: Transition {
- Anim {
- property: "opacity"
- to: 0
- }
- Anim {
- property: "scale"
- to: 0.5
- }
- }
+ const hours = Math.floor(elapsed / 3600);
+ const mins = Math.floor((elapsed % 3600) / 60);
+ const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
- displaced: Transition {
- Anim {
- properties: "opacity,scale"
- to: 1
- }
- Anim {
- property: "y"
+ let time;
+ if (hours > 0)
+ time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
+ else
+ time = `${mins}:${secs}`;
+
+ return qsTr("Recording for %1").arg(time);
}
+ font.pointSize: Appearance.font.size.normal
}
- Loader {
- anchors.centerIn: parent
-
- opacity: recordingList.count === 0 ? 1 : 0
- active: opacity > 0
- asynchronous: true
-
- sourceComponent: ColumnLayout {
- spacing: Appearance.spacing.small
-
- MaterialIcon {
- Layout.alignment: Qt.AlignHCenter
- text: "scan_delete"
- color: Colours.palette.m3outline
- font.pointSize: Appearance.font.size.extraLarge
-
- opacity: root.props.recordingListExpanded ? 1 : 0
- scale: root.props.recordingListExpanded ? 1 : 0
- Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
-
- Behavior on opacity {
- Anim {}
- }
-
- Behavior on scale {
- Anim {}
- }
-
- Behavior on Layout.preferredHeight {
- Anim {}
- }
- }
-
- RowLayout {
- spacing: Appearance.spacing.smaller
-
- MaterialIcon {
- Layout.alignment: Qt.AlignHCenter
- text: "scan_delete"
- color: Colours.palette.m3outline
-
- opacity: !root.props.recordingListExpanded ? 1 : 0
- scale: !root.props.recordingListExpanded ? 1 : 0
- Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
-
- Behavior on opacity {
- Anim {}
- }
-
- Behavior on scale {
- Anim {}
- }
-
- Behavior on Layout.preferredWidth {
- Anim {}
- }
- }
-
- StyledText {
- text: qsTr("No recordings found")
- color: Colours.palette.m3outline
- }
- }
- }
+ Item {
+ Layout.fillWidth: true
+ }
- Behavior on opacity {
- Anim {}
+ IconButton {
+ label.animate: true
+ icon: Recorder.paused ? "play_arrow" : "pause"
+ toggle: true
+ checked: Recorder.paused
+ type: IconButton.Tonal
+ font.pointSize: Appearance.font.size.large
+ onClicked: {
+ Recorder.togglePause();
+ internalChecked = Recorder.paused;
}
}
- Behavior on implicitHeight {
- Anim {
- duration: Appearance.anim.durations.expressiveDefaultSpatial
- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
- }
+ IconButton {
+ icon: "stop"
+ inactiveColour: Colours.palette.m3error
+ inactiveOnColour: Colours.palette.m3onError
+ font.pointSize: Appearance.font.size.large
+ onClicked: Recorder.stop()
}
}
}
diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml
new file mode 100644
index 0000000..1250ab3
--- /dev/null
+++ b/modules/utilities/cards/RecordingList.qml
@@ -0,0 +1,237 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.components.containers
+import qs.services
+import qs.config
+import qs.utils
+import Caelestia
+import Caelestia.Models
+import Quickshell
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property var props
+ required property var visibilities
+
+ spacing: 0
+
+ WrapperMouseArea {
+ Layout.fillWidth: true
+
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
+
+ RowLayout {
+ spacing: Appearance.spacing.smaller
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignVCenter
+ text: "list"
+ font.pointSize: Appearance.font.size.large
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ text: qsTr("Recordings")
+ font.pointSize: Appearance.font.size.normal
+ }
+
+ IconButton {
+ icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
+ type: IconButton.Text
+ label.animate: true
+ onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
+ }
+ }
+ }
+
+ StyledListView {
+ id: list
+
+ model: FileSystemModel {
+ path: Paths.recsdir
+ nameFilters: ["recording_*.mp4"]
+ sortReverse: true
+ }
+
+ Layout.fillWidth: true
+ Layout.rightMargin: -Appearance.spacing.small
+ implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
+ clip: true
+
+ StyledScrollBar.vertical: StyledScrollBar {}
+
+ delegate: RowLayout {
+ id: recording
+
+ required property FileSystemEntry modelData
+ property string baseName
+
+ anchors.left: list.contentItem.left
+ anchors.right: list.contentItem.right
+ anchors.rightMargin: Appearance.spacing.small
+ spacing: Appearance.spacing.small / 2
+
+ Component.onCompleted: baseName = modelData.baseName
+
+ StyledText {
+ Layout.fillWidth: true
+ Layout.rightMargin: Appearance.spacing.small / 2
+ text: {
+ const time = recording.baseName;
+ const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
+ if (!matches)
+ return time;
+ const date = new Date(...matches.slice(1));
+ return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
+ }
+ color: Colours.palette.m3onSurfaceVariant
+ elide: Text.ElideRight
+ }
+
+ IconButton {
+ icon: "play_arrow"
+ type: IconButton.Text
+ onClicked: {
+ root.visibilities.utilities = false;
+ Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
+ }
+ }
+
+ IconButton {
+ icon: "folder"
+ type: IconButton.Text
+ onClicked: {
+ root.visibilities.utilities = false;
+ Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
+ }
+ }
+
+ IconButton {
+ icon: "delete_forever"
+ type: IconButton.Text
+ label.color: Colours.palette.m3error
+ stateLayer.color: Colours.palette.m3error
+ onClicked: root.props.recordingConfirmDelete = recording.modelData.path
+ }
+ }
+
+ add: Transition {
+ Anim {
+ property: "opacity"
+ from: 0
+ to: 1
+ }
+ Anim {
+ property: "scale"
+ from: 0.5
+ to: 1
+ }
+ }
+
+ remove: Transition {
+ Anim {
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ property: "scale"
+ to: 0.5
+ }
+ }
+
+ displaced: Transition {
+ Anim {
+ properties: "opacity,scale"
+ to: 1
+ }
+ Anim {
+ property: "y"
+ }
+ }
+
+ Loader {
+ anchors.centerIn: parent
+
+ opacity: list.count === 0 ? 1 : 0
+ active: opacity > 0
+ asynchronous: true
+
+ sourceComponent: ColumnLayout {
+ spacing: Appearance.spacing.small
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: "scan_delete"
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.extraLarge
+
+ opacity: root.props.recordingListExpanded ? 1 : 0
+ scale: root.props.recordingListExpanded ? 1 : 0
+ Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ Behavior on Layout.preferredHeight {
+ Anim {}
+ }
+ }
+
+ RowLayout {
+ spacing: Appearance.spacing.smaller
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: "scan_delete"
+ color: Colours.palette.m3outline
+
+ opacity: !root.props.recordingListExpanded ? 1 : 0
+ scale: !root.props.recordingListExpanded ? 1 : 0
+ Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ Behavior on Layout.preferredWidth {
+ Anim {}
+ }
+ }
+
+ StyledText {
+ text: qsTr("No recordings found")
+ color: Colours.palette.m3outline
+ }
+ }
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ Behavior on implicitHeight {
+ Anim {
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+ }
+}
diff --git a/services/Recorder.qml b/services/Recorder.qml
index d5c99ca..e4ce6a8 100644
--- a/services/Recorder.qml
+++ b/services/Recorder.qml
@@ -2,23 +2,33 @@ pragma Singleton
import Quickshell
import Quickshell.Io
+import QtQuick
Singleton {
id: root
readonly property alias running: props.running
readonly property alias paused: props.paused
+ readonly property alias elapsed: props.elapsed
+ property bool needsStart
+ property list<string> startArgs
+ property bool needsStop
+ property bool needsPause
- function toggle(extraArgs: list<string>): void {
- Quickshell.execDetached(["caelestia", "record", ...extraArgs]);
- props.running = !props.running;
- if (!props.running)
- props.paused = false;
+ function start(extraArgs: list<string>): void {
+ needsStart = true;
+ startArgs = extraArgs;
+ checkProc.running = true;
+ }
+
+ function stop(): void {
+ needsStop = true;
+ checkProc.running = true;
}
function togglePause(): void {
- Quickshell.execDetached(["caelestia", "record", "-p"]);
- props.paused = !props.paused;
+ needsPause = true;
+ checkProc.running = true;
}
PersistentProperties {
@@ -26,13 +36,47 @@ Singleton {
property bool running: false
property bool paused: false
+ property real elapsed: 0 // Might get too large for int
reloadableId: "recorder"
}
Process {
+ id: checkProc
+
running: true
command: ["pidof", "gpu-screen-recorder"]
- onExited: code => props.running = code === 0
+ onExited: code => {
+ props.running = code === 0;
+
+ if (code === 0) {
+ if (root.needsStop) {
+ Quickshell.execDetached(["caelestia", "record"]);
+ props.running = false;
+ props.paused = false;
+ } else if (root.needsPause) {
+ Quickshell.execDetached(["caelestia", "record", "-p"]);
+ props.paused = !props.paused;
+ }
+ } else if (root.needsStart) {
+ Quickshell.execDetached(["caelestia", "record", ...root.startArgs]);
+ props.running = true;
+ props.paused = false;
+ props.elapsed = 0;
+ }
+
+ root.needsStart = false;
+ root.needsStop = false;
+ root.needsPause = false;
+ }
+ }
+
+ Connections {
+ target: Time
+ // enabled: props.running && !props.paused
+
+ function onSecondsChanged(): void {
+ props.elapsed++;
+ }
}
}