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 alias icon: label.text
property bool checked property bool checked
property bool toggle 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 alias font: label.font
property int type: IconButton.Filled property int type: IconButton.Filled
@ -23,18 +23,25 @@ StyledRect {
property alias label: label property alias label: label
property bool internalChecked property bool internalChecked
property color activeColour: type == IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary 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 inactiveColour: {
property color activeOnColour: type == IconButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary if (!toggle && type === IconButton.Filled)
property color inactiveOnColour: type == IconButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer return Colours.palette.m3primary;
return type === IconButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer;
function onClicked(): void {
} }
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 onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 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 implicitWidth: implicitHeight
implicitHeight: label.implicitHeight + padding * 2 implicitHeight: label.implicitHeight + padding * 2
@ -47,7 +54,7 @@ StyledRect {
function onClicked(): void { function onClicked(): void {
if (root.toggle) if (root.toggle)
root.internalChecked = !root.internalChecked; root.internalChecked = !root.internalChecked;
root.onClicked(); root.clicked();
} }
} }
@ -56,7 +63,7 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
fill: root.internalChecked ? 1 : 0 fill: !root.toggle || root.internalChecked ? 1 : 0
Behavior on fill { Behavior on fill {
Anim {} Anim {}

View file

@ -26,18 +26,17 @@ StyledRect {
property alias label: label property alias label: label
property bool internalChecked property bool internalChecked
property color activeColour: type == IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary 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 inactiveColour: type === IconTextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
property color activeOnColour: type == IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary 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 inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
function onClicked(): void { signal clicked
}
onCheckedChanged: internalChecked = checked onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 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 implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2 implicitHeight: row.implicitHeight + verticalPadding * 2
@ -50,7 +49,7 @@ StyledRect {
function onClicked(): void { function onClicked(): void {
if (root.toggle) if (root.toggle)
root.internalChecked = !root.internalChecked; root.internalChecked = !root.internalChecked;
root.onClicked(); root.clicked();
} }
} }

View file

@ -15,6 +15,7 @@ Row {
property real horizontalPadding: Appearance.padding.normal property real horizontalPadding: Appearance.padding.normal
property real verticalPadding: Appearance.padding.smaller property real verticalPadding: Appearance.padding.smaller
property int type: SplitButton.Filled property int type: SplitButton.Filled
property bool disabled
property alias menuItems: menu.items property alias menuItems: menu.items
property alias active: menu.active property alias active: menu.active
property alias expanded: menu.expanded property alias expanded: menu.expanded
@ -22,6 +23,8 @@ Row {
property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer 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) spacing: Math.floor(Appearance.spacing.small / 2)
@ -29,7 +32,7 @@ Row {
radius: implicitHeight / 2 radius: implicitHeight / 2
topRightRadius: Appearance.rounding.small / 2 topRightRadius: Appearance.rounding.small / 2
bottomRightRadius: 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 implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
implicitHeight: expandBtn.implicitHeight implicitHeight: expandBtn.implicitHeight
@ -40,6 +43,7 @@ Row {
rect.topRightRadius: parent.topRightRadius rect.topRightRadius: parent.topRightRadius
rect.bottomRightRadius: parent.bottomRightRadius rect.bottomRightRadius: parent.bottomRightRadius
color: root.textColour color: root.textColour
disabled: root.disabled
function onClicked(): void { function onClicked(): void {
root.active?.clicked(); root.active?.clicked();
@ -59,7 +63,7 @@ Row {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
text: root.active?.activeIcon ?? "" text: root.active?.activeIcon ?? ""
color: root.textColour color: root.disabled ? root.disabledTextColour : root.textColour
fill: 1 fill: 1
} }
@ -70,7 +74,7 @@ Row {
Layout.preferredWidth: implicitWidth Layout.preferredWidth: implicitWidth
animate: true animate: true
text: root.active?.activeText ?? "" text: root.active?.activeText ?? ""
color: root.textColour color: root.disabled ? root.disabledTextColour : root.textColour
clip: true clip: true
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {
@ -90,7 +94,7 @@ Row {
radius: implicitHeight / 2 radius: implicitHeight / 2
topLeftRadius: rad topLeftRadius: rad
bottomLeftRadius: rad bottomLeftRadius: rad
color: root.colour color: root.disabled ? root.disabledColour : root.colour
implicitWidth: implicitHeight implicitWidth: implicitHeight
implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2 implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2
@ -101,6 +105,7 @@ Row {
rect.topRightRadius: parent.topRightRadius rect.topRightRadius: parent.topRightRadius
rect.bottomRightRadius: parent.bottomRightRadius rect.bottomRightRadius: parent.bottomRightRadius
color: root.textColour color: root.textColour
disabled: root.disabled
function onClicked(): void { function onClicked(): void {
root.expanded = !root.expanded; root.expanded = !root.expanded;
@ -114,7 +119,7 @@ Row {
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4) anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
text: "expand_more" text: "expand_more"
color: root.textColour color: root.disabled ? root.disabledTextColour : root.textColour
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 180 : 0
Behavior on anchors.horizontalCenterOffset { Behavior on anchors.horizontalCenterOffset {

View file

@ -24,18 +24,17 @@ StyledRect {
property alias label: label property alias label: label
property bool internalChecked property bool internalChecked
property color activeColour: type == TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary 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 inactiveColour: type === TextButton.Filled ? Colours.palette.m3surfaceContainer : Colours.palette.m3secondaryContainer
property color activeOnColour: type == TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary 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 inactiveOnColour: type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
function onClicked(): void { signal clicked
}
onCheckedChanged: internalChecked = checked onCheckedChanged: internalChecked = checked
radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 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 implicitWidth: label.implicitWidth + horizontalPadding * 2
implicitHeight: label.implicitHeight + verticalPadding * 2 implicitHeight: label.implicitHeight + verticalPadding * 2
@ -48,7 +47,7 @@ StyledRect {
function onClicked(): void { function onClicked(): void {
if (root.toggle) if (root.toggle)
root.internalChecked = !root.internalChecked; root.internalChecked = !root.internalChecked;
root.onClicked(); root.clicked();
} }
} }

View file

@ -181,10 +181,7 @@ Loader {
type: TextButton.Text type: TextButton.Text
label.color: Colours.palette.m3primary label.color: Colours.palette.m3primary
stateLayer.color: Colours.palette.m3primary stateLayer.color: Colours.palette.m3primary
onClicked: root.props.recordingConfirmDelete = ""
function onClicked(): void {
root.props.recordingConfirmDelete = "";
}
} }
TextButton { TextButton {
@ -192,8 +189,7 @@ Loader {
type: TextButton.Text type: TextButton.Text
label.color: Colours.palette.m3primary label.color: Colours.palette.m3primary
stateLayer.color: Colours.palette.m3primary stateLayer.color: Colours.palette.m3primary
onClicked: {
function onClicked(): void {
CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete)); CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete));
root.props.recordingConfirmDelete = ""; root.props.recordingConfirmDelete = "";
} }

View file

@ -2,14 +2,8 @@ pragma ComponentBehavior: Bound
import qs.components import qs.components
import qs.components.controls import qs.components.controls
import qs.components.containers
import qs.services import qs.services
import qs.config import qs.config
import qs.utils
import Caelestia
import Caelestia.Models
import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@ -18,7 +12,6 @@ StyledRect {
required property var props required property var props
required property var visibilities required property var visibilities
property bool modePickerExpanded
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
@ -80,6 +73,7 @@ StyledRect {
} }
SplitButton { SplitButton {
disabled: Recorder.running
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0] active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
@ -88,250 +82,181 @@ StyledRect {
icon: "fullscreen" icon: "fullscreen"
text: qsTr("Record fullscreen") text: qsTr("Record fullscreen")
activeText: qsTr("Fullscreen") activeText: qsTr("Fullscreen")
onClicked: Recorder.toggle() onClicked: Recorder.start()
}, },
MenuItem { MenuItem {
icon: "screenshot_region" icon: "screenshot_region"
text: qsTr("Record region") text: qsTr("Record region")
activeText: qsTr("Region") activeText: qsTr("Region")
onClicked: Recorder.toggle(["-r"]) onClicked: Recorder.start(["-r"])
}, },
MenuItem { MenuItem {
icon: "select_to_speak" icon: "select_to_speak"
text: qsTr("Record fullscreen with sound") text: qsTr("Record fullscreen with sound")
activeText: qsTr("Fullscreen") activeText: qsTr("Fullscreen")
onClicked: Recorder.toggle(["-s"]) onClicked: Recorder.start(["-s"])
}, },
MenuItem { MenuItem {
icon: "volume_up" icon: "volume_up"
text: qsTr("Record region with sound") text: qsTr("Record region with sound")
activeText: qsTr("Region") activeText: qsTr("Region")
onClicked: Recorder.toggle(["-sr"]) onClicked: Recorder.start(["-sr"])
} }
] ]
} }
} }
WrapperMouseArea { Loader {
Layout.bottomMargin: -layout.spacing id: listOrControls
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor property bool running: Recorder.running
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
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: -Appearance.spacing.small Layout.preferredHeight: implicitHeight
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) sourceComponent: running ? recordingControls : recordingList
clip: true
StyledScrollBar.vertical: StyledScrollBar {} Behavior on Layout.preferredHeight {
Anim {}
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;
}
}
} }
add: Transition { Behavior on running {
Anim { SequentialAnimation {
property: "opacity" ParallelAnimation {
from: 0 Anim {
to: 1 target: listOrControls
} property: "scale"
Anim { to: 0.7
property: "scale" duration: Appearance.anim.durations.small
from: 0.5 easing.bezierCurve: Appearance.anim.curves.standardAccel
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 {}
} }
Anim {
Behavior on scale { target: listOrControls
Anim {} property: "opacity"
} to: 0
duration: Appearance.anim.durations.small
Behavior on Layout.preferredHeight { easing.bezierCurve: Appearance.anim.curves.standardAccel
Anim {}
} }
} }
PropertyAction {}
RowLayout { ParallelAnimation {
spacing: Appearance.spacing.smaller Anim {
target: listOrControls
MaterialIcon { property: "scale"
Layout.alignment: Qt.AlignHCenter to: 1
text: "scan_delete" duration: Appearance.anim.durations.small
color: Colours.palette.m3outline easing.bezierCurve: Appearance.anim.curves.standardDecel
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 {}
}
} }
Anim {
StyledText { target: listOrControls
text: qsTr("No recordings found") property: "opacity"
color: Colours.palette.m3outline to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.standardDecel
} }
} }
} }
}
Behavior on opacity { }
Anim {} }
}
} Component {
id: recordingList
Behavior on implicitHeight {
Anim { RecordingList {
duration: Appearance.anim.durations.expressiveDefaultSpatial props: root.props
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial 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
import Quickshell.Io import Quickshell.Io
import QtQuick
Singleton { Singleton {
id: root id: root
readonly property alias running: props.running readonly property alias running: props.running
readonly property alias paused: props.paused 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 { function start(extraArgs: list<string>): void {
Quickshell.execDetached(["caelestia", "record", ...extraArgs]); needsStart = true;
props.running = !props.running; startArgs = extraArgs;
if (!props.running) checkProc.running = true;
props.paused = false; }
function stop(): void {
needsStop = true;
checkProc.running = true;
} }
function togglePause(): void { function togglePause(): void {
Quickshell.execDetached(["caelestia", "record", "-p"]); needsPause = true;
props.paused = !props.paused; checkProc.running = true;
} }
PersistentProperties { PersistentProperties {
@ -26,13 +36,47 @@ Singleton {
property bool running: false property bool running: false
property bool paused: false property bool paused: false
property real elapsed: 0 // Might get too large for int
reloadableId: "recorder" reloadableId: "recorder"
} }
Process { Process {
id: checkProc
running: true running: true
command: ["pidof", "gpu-screen-recorder"] 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++;
}
} }
} }