diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-15 21:55:10 +1000 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-15 21:55:10 +1000 |
| commit | 7a9fce9dd417db42d75dc80a86df45ed0402d6d4 (patch) | |
| tree | 1a3671200433275f017073e70e895971bfd94951 | |
| parent | utilities/record: select mode (diff) | |
| download | caelestia-shell-7a9fce9dd417db42d75dc80a86df45ed0402d6d4.tar.gz caelestia-shell-7a9fce9dd417db42d75dc80a86df45ed0402d6d4.tar.bz2 caelestia-shell-7a9fce9dd417db42d75dc80a86df45ed0402d6d4.zip | |
utilities/record: allow pause/resume recording
| -rw-r--r-- | components/controls/IconButton.qml | 27 | ||||
| -rw-r--r-- | components/controls/IconTextButton.qml | 15 | ||||
| -rw-r--r-- | components/controls/SplitButton.qml | 15 | ||||
| -rw-r--r-- | components/controls/TextButton.qml | 15 | ||||
| -rw-r--r-- | modules/utilities/RecordingDeleteModal.qml | 8 | ||||
| -rw-r--r-- | modules/utilities/cards/Record.qml | 317 | ||||
| -rw-r--r-- | modules/utilities/cards/RecordingList.qml | 237 | ||||
| -rw-r--r-- | services/Recorder.qml | 60 |
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++; + } } } |