utilities/record: allow pause/resume recording
This commit is contained in:
parent
4f54763e02
commit
7a9fce9dd4
8 changed files with 475 additions and 263 deletions
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
237
modules/utilities/cards/RecordingList.qml
Normal file
237
modules/utilities/cards/RecordingList.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue