utilities/record: allow pause/resume recording

This commit is contained in:
2 * r + 2 * t 2025-09-15 21:55:10 +10:00
parent 4f54763e02
commit 7a9fce9dd4
8 changed files with 475 additions and 263 deletions

View file

@ -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 {}

View file

@ -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();
}
}

View file

@ -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 {

View file

@ -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();
}
}

View file

@ -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 = "";
}

View file

@ -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
Loader {
id: listOrControls
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
function onClicked(): void {
root.props.recordingListExpanded = !root.props.recordingListExpanded;
}
}
}
}
StyledListView {
id: recordingList
model: FileSystemModel {
path: Paths.recsdir
nameFilters: ["recording_*.mp4"]
sortReverse: true
}
property bool running: Recorder.running
Layout.fillWidth: true
Layout.rightMargin: -Appearance.spacing.small
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
clip: true
Layout.preferredHeight: implicitHeight
sourceComponent: running ? recordingControls : recordingList
StyledScrollBar.vertical: StyledScrollBar {}
delegate: RowLayout {
id: recording
required property FileSystemEntry modelData
property string baseName
anchors.left: recordingList.contentItem.left
anchors.right: recordingList.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
function onClicked(): void {
root.visibilities.utilities = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
}
}
IconButton {
icon: "folder"
type: IconButton.Text
function onClicked(): void {
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
function onClicked(): void {
root.props.recordingConfirmDelete = recording.modelData.path;
}
}
Behavior on Layout.preferredHeight {
Anim {}
}
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: 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 running {
SequentialAnimation {
ParallelAnimation {
Anim {
target: listOrControls
property: "scale"
to: 0.7
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Behavior on scale {
Anim {}
}
Behavior on Layout.preferredHeight {
Anim {}
Anim {
target: listOrControls
property: "opacity"
to: 0
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
}
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 {}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: listOrControls
property: "scale"
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
StyledText {
text: qsTr("No recordings found")
color: Colours.palette.m3outline
Anim {
target: listOrControls
property: "opacity"
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
Behavior on opacity {
Anim {}
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}
Component {
id: recordingList
RecordingList {
props: root.props
visibilities: root.visibilities
}
}
Component {
id: recordingControls
RowLayout {
spacing: Appearance.spacing.normal
StyledRect {
radius: Appearance.rounding.full
color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error
implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
StyledText {
id: recText
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
}
Behavior on implicitWidth {
Anim {}
}
SequentialAnimation on opacity {
running: !Recorder.paused
alwaysRunToEnd: true
loops: Animation.Infinite
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
}
}
}
StyledText {
text: {
const elapsed = Recorder.elapsed;
const hours = Math.floor(elapsed / 3600);
const mins = Math.floor((elapsed % 3600) / 60);
const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
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
}
Item {
Layout.fillWidth: true
}
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;
}
}
IconButton {
icon: "stop"
inactiveColour: Colours.palette.m3error
inactiveOnColour: Colours.palette.m3onError
font.pointSize: Appearance.font.size.large
onClicked: Recorder.stop()
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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++;
}
}
}