Record module added to sidebar, file list and buttons. Region recording is broken
This commit is contained in:
+82
-41
@@ -1,41 +1,82 @@
|
|||||||
// pragma Singleton
|
pragma Singleton
|
||||||
//
|
|
||||||
// import Quickshell
|
import Quickshell
|
||||||
// import QtQuick
|
import Quickshell.Io
|
||||||
//
|
import QtQuick
|
||||||
// Singleton {
|
|
||||||
// id: root
|
Singleton {
|
||||||
//
|
id: root
|
||||||
// function start(extraArgs = []): void {
|
|
||||||
// needsStart = true;
|
readonly property alias elapsed: props.elapsed
|
||||||
// startArgs = extraArgs;
|
property bool needsPause
|
||||||
// checkProc.running = true;
|
property bool needsStart
|
||||||
// }
|
property bool needsStop
|
||||||
//
|
readonly property alias paused: props.paused
|
||||||
// PersistentProperties {
|
readonly property alias running: props.running
|
||||||
// id: props
|
property list<string> startArgs
|
||||||
//
|
|
||||||
// property real elapsed: 0
|
function start(extraArgs = []): void {
|
||||||
// property bool paused: false
|
needsStart = true;
|
||||||
// property bool running: false
|
startArgs = extraArgs;
|
||||||
//
|
checkProc.running = true;
|
||||||
// reloadableId: "recorder"
|
}
|
||||||
// }
|
|
||||||
//
|
function stop(): void {
|
||||||
// Process {
|
needsStop = true;
|
||||||
// id: checkProc
|
checkProc.running = true;
|
||||||
//
|
}
|
||||||
// command: ["pidof", "gpu-screen-recorder"]
|
|
||||||
// running: true
|
function togglePause(): void {
|
||||||
//
|
needsPause = true;
|
||||||
// onExited: code => {
|
checkProc.running = true;
|
||||||
// props.running = code === 0;
|
}
|
||||||
//
|
|
||||||
// if (code === 0) {
|
PersistentProperties {
|
||||||
// if (root.needsStop) {
|
id: props
|
||||||
// Quickshell.execDetached(["zshell-cli"]);
|
|
||||||
// }
|
property real elapsed: 0
|
||||||
// }
|
property bool paused: false
|
||||||
// }
|
property bool running: false
|
||||||
// }
|
|
||||||
// }
|
reloadableId: "recorder"
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: checkProc
|
||||||
|
|
||||||
|
command: ["pidof", "gpu-screen-recorder"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
onExited: code => {
|
||||||
|
props.running = code === 0;
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
if (root.needsStop) {
|
||||||
|
Quickshell.execDetached(["zshell-cli", "record", "record"]);
|
||||||
|
props.running = false;
|
||||||
|
props.paused = false;
|
||||||
|
} else if (root.needsPause) {
|
||||||
|
Quickshell.execDetached(["zshell-cli", "record", "record", "-p"]);
|
||||||
|
props.paused = !props.paused;
|
||||||
|
}
|
||||||
|
} else if (root.needsStart) {
|
||||||
|
Quickshell.execDetached(["zshell-cli", "record", "record", ...root.startArgs]);
|
||||||
|
props.running = true;
|
||||||
|
props.paused = false;
|
||||||
|
props.elapsed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.needsStart = false;
|
||||||
|
root.needsStop = false;
|
||||||
|
root.needsPause = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onSecondsChanged(): void {
|
||||||
|
props.elapsed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Time // qmllint disable incompatible-type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var props
|
||||||
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||||
|
radius: Appearance.rounding.smallest
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
z: 1
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
color: Recorder.running ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer
|
||||||
|
implicitHeight: {
|
||||||
|
const h = icon.implicitHeight + Appearance.padding.smaller * 2;
|
||||||
|
return h - (h % 2);
|
||||||
|
}
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.horizontalCenterOffset: -0.5
|
||||||
|
anchors.verticalCenterOffset: 1.5
|
||||||
|
color: Recorder.running ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: "screen_record"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
text: qsTr("Screen Recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSplitButton {
|
||||||
|
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
|
||||||
|
disabled: Recorder.running
|
||||||
|
|
||||||
|
menuItems: [
|
||||||
|
MenuItem {
|
||||||
|
activeText: qsTr("Fullscreen")
|
||||||
|
icon: "fullscreen"
|
||||||
|
text: qsTr("Record fullscreen")
|
||||||
|
|
||||||
|
onClicked: Recorder.start()
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
activeText: qsTr("Region")
|
||||||
|
icon: "screenshot_region"
|
||||||
|
text: qsTr("Record region")
|
||||||
|
|
||||||
|
onClicked: Recorder.start(["-r"])
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
activeText: qsTr("Fullscreen")
|
||||||
|
icon: "select_to_speak"
|
||||||
|
text: qsTr("Record fullscreen with sound")
|
||||||
|
|
||||||
|
onClicked: Recorder.start(["-s"])
|
||||||
|
},
|
||||||
|
MenuItem {
|
||||||
|
activeText: qsTr("Region")
|
||||||
|
icon: "volume_up"
|
||||||
|
text: qsTr("Record region with sound")
|
||||||
|
|
||||||
|
onClicked: Recorder.start(["-s", "-r"])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: listOrControls
|
||||||
|
|
||||||
|
property bool running: Recorder.running
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: implicitHeight
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: running ? recordingControls : recordingList
|
||||||
|
|
||||||
|
Behavior on Layout.preferredHeight {
|
||||||
|
id: locHeightAnim
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on running {
|
||||||
|
SequentialAnimation {
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing: Appearance.anim.curves.standardAccel
|
||||||
|
property: "scale"
|
||||||
|
target: listOrControls
|
||||||
|
to: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing: Appearance.anim.curves.standardAccel
|
||||||
|
property: "opacity"
|
||||||
|
target: listOrControls
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
property: "enabled"
|
||||||
|
target: locHeightAnim
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
property: "enabled"
|
||||||
|
target: locHeightAnim
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing: Appearance.anim.curves.standardDecel
|
||||||
|
property: "scale"
|
||||||
|
target: listOrControls
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing: Appearance.anim.curves.standardDecel
|
||||||
|
property: "opacity"
|
||||||
|
target: listOrControls
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: recordingList
|
||||||
|
|
||||||
|
RecordingList {
|
||||||
|
props: root.props
|
||||||
|
visibilities: root.visibilities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: recordingControls
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
color: Recorder.paused ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3error
|
||||||
|
implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
|
||||||
|
implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
alwaysRunToEnd: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: !Recorder.paused
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.large
|
||||||
|
easing: Appearance.anim.curves.emphasizedAccel
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.extraLarge
|
||||||
|
easing: Appearance.anim.curves.emphasizedDecel
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: recText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
animate: true
|
||||||
|
color: Recorder.paused ? DynamicColors.palette.m3onTertiary : DynamicColors.palette.m3onError
|
||||||
|
font.family: Appearance.font.family.mono
|
||||||
|
text: Recorder.paused ? "PAUSED" : "REC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
checked: Recorder.paused
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: Recorder.paused ? "play_arrow" : "pause"
|
||||||
|
label.animate: true
|
||||||
|
toggle: true
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Recorder.togglePause();
|
||||||
|
internalChecked = Recorder.paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: "stop"
|
||||||
|
inactiveColour: DynamicColors.palette.m3error
|
||||||
|
inactiveOnColour: DynamicColors.palette.m3onError
|
||||||
|
|
||||||
|
onClicked: Recorder.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import ZShell.Models
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Paths
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var props
|
||||||
|
required property PersistentProperties 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
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
text: qsTr("Recordings")
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
|
||||||
|
label.animate: true
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
|
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomListView {
|
||||||
|
id: list
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: -Appearance.spacing.small
|
||||||
|
clip: true
|
||||||
|
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
|
||||||
|
|
||||||
|
CustomScrollBar.vertical: CustomScrollBar {
|
||||||
|
flickable: list
|
||||||
|
}
|
||||||
|
add: Transition {
|
||||||
|
Anim {
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
from: 0.5
|
||||||
|
property: "scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate: RowLayout {
|
||||||
|
id: recording
|
||||||
|
|
||||||
|
property string baseName
|
||||||
|
required property FileSystemEntry modelData
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: Appearance.spacing.small / 2
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
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));
|
||||||
|
date.setMonth(date.getMonth() - 1);
|
||||||
|
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "play_arrow"
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.visibilities.sidebar = false;
|
||||||
|
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "folder"
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.visibilities.sidebar = false;
|
||||||
|
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displaced: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "opacity,scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model: FileSystemModel {
|
||||||
|
nameFilters: ["recording_*.mp4"]
|
||||||
|
path: Paths.recsdir
|
||||||
|
sortReverse: true
|
||||||
|
}
|
||||||
|
remove: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
property: "scale"
|
||||||
|
to: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
asynchronous: true
|
||||||
|
opacity: list.count === 0 ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceComponent: ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
|
opacity: root.props.recordingListExpanded ? 1 : 0
|
||||||
|
scale: root.props.recordingListExpanded ? 1 : 0
|
||||||
|
text: "scan_delete"
|
||||||
|
|
||||||
|
Behavior on Layout.preferredHeight {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
opacity: !root.props.recordingListExpanded ? 1 : 0
|
||||||
|
scale: !root.props.recordingListExpanded ? 1 : 0
|
||||||
|
text: "scan_delete"
|
||||||
|
|
||||||
|
Behavior on Layout.preferredWidth {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
text: qsTr("No recordings found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
import Quickshell
|
||||||
import qs.Config
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Item popouts
|
required property Item popouts
|
||||||
required property var props
|
required property PersistentProperties props
|
||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
implicitHeight: layout.implicitHeight
|
implicitHeight: layout.implicitHeight
|
||||||
@@ -22,6 +23,12 @@ Item {
|
|||||||
IdleInhibit {
|
IdleInhibit {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Record {
|
||||||
|
props: root.props
|
||||||
|
visibilities: root.visibilities
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
Toggles {
|
Toggles {
|
||||||
popouts: root.popouts
|
popouts: root.popouts
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"typer",
|
"typer",
|
||||||
"pillow",
|
"pillow",
|
||||||
|
"jinja2",
|
||||||
"materialyoucolor"
|
"materialyoucolor"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,6 @@ import os
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import signal
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -15,19 +14,15 @@ RECORDER = "gpu-screen-recorder"
|
|||||||
HOME = str(os.getenv("HOME", str(Path.home())))
|
HOME = str(os.getenv("HOME", str(Path.home())))
|
||||||
CONFIG = Path(HOME) / ".config/zshell/config.json"
|
CONFIG = Path(HOME) / ".config/zshell/config.json"
|
||||||
|
|
||||||
# Paths for temp recording and notifications
|
|
||||||
STATE_DIR = Path(HOME) / ".local/state/zshell/record"
|
STATE_DIR = Path(HOME) / ".local/state/zshell/record"
|
||||||
TEMP_RECORDING = STATE_DIR / "recording.mp4"
|
TEMP_RECORDING = STATE_DIR / "recording.mp4"
|
||||||
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
|
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
|
||||||
|
|
||||||
# Where final recordings are saved
|
|
||||||
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
|
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
|
||||||
str(Path(HOME) / "Videos/Recordings"))
|
str(Path(HOME) / "Videos/Recordings"))
|
||||||
|
|
||||||
|
|
||||||
# ── helpers ──────────────────────────────────────────────
|
|
||||||
def _read_extra_args() -> list[str]:
|
def _read_extra_args() -> list[str]:
|
||||||
"""Return extra gpu-screen-recorder arguments from the user config."""
|
|
||||||
try:
|
try:
|
||||||
if CONFIG.is_file():
|
if CONFIG.is_file():
|
||||||
data = json.loads(CONFIG.read_text())
|
data = json.loads(CONFIG.read_text())
|
||||||
@@ -38,12 +33,10 @@ def _read_extra_args() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _is_recording() -> bool:
|
def _is_recording() -> bool:
|
||||||
"""Check if gpu-screen-recorder process exists."""
|
|
||||||
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 3000) -> Optional[int]:
|
def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 5000) -> Optional[int]:
|
||||||
"""Send a desktop notification. Returns the notification ID or None."""
|
|
||||||
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
|
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
|
||||||
if actions:
|
if actions:
|
||||||
for action in actions:
|
for action in actions:
|
||||||
@@ -56,13 +49,11 @@ def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 3
|
|||||||
|
|
||||||
|
|
||||||
def _close_notification(notif_id: int):
|
def _close_notification(notif_id: int):
|
||||||
"""Close a notification by its ID."""
|
|
||||||
subprocess.run(["notify-send", "--close", str(notif_id)],
|
subprocess.run(["notify-send", "--close", str(notif_id)],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
def _get_monitors() -> list[dict]:
|
def _get_monitors() -> list[dict]:
|
||||||
"""Get monitor info from Hyprland."""
|
|
||||||
try:
|
try:
|
||||||
res = subprocess.run(["hyprctl", "monitors", "-j"],
|
res = subprocess.run(["hyprctl", "monitors", "-j"],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
@@ -72,16 +63,13 @@ def _get_monitors() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def _focused_monitor_name() -> Optional[str]:
|
def _focused_monitor_name() -> Optional[str]:
|
||||||
"""Return name of the currently focused monitor."""
|
for m in _get_monitors():
|
||||||
monitors = _get_monitors()
|
|
||||||
for m in monitors:
|
|
||||||
if m.get("focused"):
|
if m.get("focused"):
|
||||||
return m["name"]
|
return m["name"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
||||||
"""Return monitors whose area intersects the given region."""
|
|
||||||
region = (x, y, x + w, y + h)
|
region = (x, y, x + w, y + h)
|
||||||
intersecting = []
|
intersecting = []
|
||||||
for m in _get_monitors():
|
for m in _get_monitors():
|
||||||
@@ -92,12 +80,10 @@ def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def _highest_refresh(monitors: list[dict]) -> float:
|
def _highest_refresh(monitors: list[dict]) -> float:
|
||||||
"""Return the maximum refresh rate among the given monitors."""
|
|
||||||
return max((m["refreshRate"] for m in monitors), default=60.0)
|
return max((m["refreshRate"] for m in monitors), default=60.0)
|
||||||
|
|
||||||
|
|
||||||
def _slurp_region() -> Optional[str]:
|
def _slurp_region() -> Optional[str]:
|
||||||
"""Call slurp and return geometry like 'WxH+X+Y'."""
|
|
||||||
try:
|
try:
|
||||||
return subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True).strip()
|
return subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True).strip()
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
@@ -105,7 +91,6 @@ def _slurp_region() -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
|
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
|
||||||
"""Parse 'WxH+X+Y' into (x,y,w,h)."""
|
|
||||||
import re
|
import re
|
||||||
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
||||||
if match:
|
if match:
|
||||||
@@ -113,12 +98,9 @@ def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ── core actions ─────────────────────────────────────────
|
|
||||||
def start_recording(region: Optional[str], sound: bool):
|
def start_recording(region: Optional[str], sound: bool):
|
||||||
"""Launch gpu-screen-recorder."""
|
|
||||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [RECORDER]
|
||||||
cmd = [RECORDER, "-w"] # -w for window/display
|
|
||||||
extra_args = _read_extra_args()
|
extra_args = _read_extra_args()
|
||||||
|
|
||||||
if region:
|
if region:
|
||||||
@@ -129,43 +111,41 @@ def start_recording(region: Optional[str], sound: bool):
|
|||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
else:
|
else:
|
||||||
geometry = region
|
geometry = region
|
||||||
|
|
||||||
parsed = _parse_geometry(geometry)
|
parsed = _parse_geometry(geometry)
|
||||||
if parsed:
|
if not parsed:
|
||||||
x, y, w, h = parsed
|
|
||||||
monitors = _monitors_intersecting_region(x, y, w, h)
|
|
||||||
framerate = _highest_refresh(monitors)
|
|
||||||
cmd.extend(["-region", geometry, "-f", str(int(framerate))])
|
|
||||||
else:
|
|
||||||
typer.echo("Invalid geometry format.")
|
typer.echo("Invalid geometry format.")
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
|
x, y, w, h = parsed
|
||||||
|
|
||||||
|
monitors = _monitors_intersecting_region(x, y, w, h)
|
||||||
|
framerate = _highest_refresh(monitors)
|
||||||
|
cmd.extend(["-w", "-region", geometry, "-f", str(int(framerate))])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Fullscreen: use focused monitor
|
monitor_name = _focused_monitor_name()
|
||||||
monitor = _focused_monitor_name()
|
if not monitor_name:
|
||||||
if monitor:
|
typer.echo("No focused monitor found.")
|
||||||
cmd.extend(["-m", monitor])
|
raise typer.Abort()
|
||||||
# Refresh rate comes from that monitor
|
|
||||||
monitors = _get_monitors()
|
monitors = _get_monitors()
|
||||||
mon = next((m for m in monitors if m["name"] == monitor), None)
|
mon = next((m for m in monitors if m["name"] == monitor_name), None)
|
||||||
if mon:
|
rate = int(mon["refreshRate"]) if mon else 60
|
||||||
cmd.extend(["-f", str(int(mon["refreshRate"]))])
|
cmd.extend(["-w", monitor_name, "-f", str(rate)])
|
||||||
|
|
||||||
if sound:
|
if sound:
|
||||||
cmd.append("-a")
|
cmd.extend(["-a", "default_output"])
|
||||||
cmd.append("default_output")
|
|
||||||
|
|
||||||
cmd.extend(extra_args)
|
cmd.extend(extra_args)
|
||||||
cmd.append(str(TEMP_RECORDING))
|
cmd.extend(["-o", str(TEMP_RECORDING)])
|
||||||
|
|
||||||
# Launch detached
|
|
||||||
subprocess.Popen(cmd, start_new_session=True,
|
subprocess.Popen(cmd, start_new_session=True,
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
# Notification
|
|
||||||
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
|
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
|
||||||
if notif_id is not None:
|
if notif_id is not None:
|
||||||
NOTIF_ID_FILE.write_text(str(notif_id))
|
NOTIF_ID_FILE.write_text(str(notif_id))
|
||||||
|
|
||||||
# Early failure check
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if not _is_recording():
|
if not _is_recording():
|
||||||
_notify("Recording failed",
|
_notify("Recording failed",
|
||||||
@@ -174,26 +154,22 @@ def start_recording(region: Optional[str], sound: bool):
|
|||||||
|
|
||||||
|
|
||||||
def stop_recording(clipboard: bool):
|
def stop_recording(clipboard: bool):
|
||||||
"""Stop the recording and finalise the file."""
|
|
||||||
# Kill the process
|
|
||||||
subprocess.run(["pkill", "-f", RECORDER],
|
subprocess.run(["pkill", "-f", RECORDER],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
# Wait until it really stops
|
for _ in range(50):
|
||||||
for _ in range(50): # 5 seconds max
|
|
||||||
if not _is_recording():
|
if not _is_recording():
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Move the recording
|
|
||||||
dest_dir = Path(RECORDINGS_DIR)
|
dest_dir = Path(RECORDINGS_DIR)
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
|
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
final_path = dest_dir / f"recording_{timestamp}.mp4"
|
final_path = dest_dir / f"recording_{timestamp}.mp4"
|
||||||
|
|
||||||
if TEMP_RECORDING.exists():
|
if TEMP_RECORDING.exists():
|
||||||
TEMP_RECORDING.rename(final_path)
|
TEMP_RECORDING.rename(final_path)
|
||||||
|
|
||||||
# Close the start notification
|
|
||||||
if NOTIF_ID_FILE.is_file():
|
if NOTIF_ID_FILE.is_file():
|
||||||
try:
|
try:
|
||||||
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
||||||
@@ -201,23 +177,19 @@ def stop_recording(clipboard: bool):
|
|||||||
pass
|
pass
|
||||||
NOTIF_ID_FILE.unlink()
|
NOTIF_ID_FILE.unlink()
|
||||||
|
|
||||||
# Clipboard
|
|
||||||
if clipboard:
|
if clipboard:
|
||||||
subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
|
subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
# Final notification (simplified: no actions)
|
|
||||||
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
|
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
|
||||||
|
|
||||||
|
|
||||||
def toggle_pause():
|
def toggle_pause():
|
||||||
"""Send SIGUSR2 to gpu-screen-recorder."""
|
|
||||||
subprocess.run(["pkill", "-USR2", "-f", RECORDER],
|
subprocess.run(["pkill", "-USR2", "-f", RECORDER],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
typer.echo("Toggled pause.")
|
typer.echo("Toggled pause.")
|
||||||
|
|
||||||
|
|
||||||
# ── Typer command ────────────────────────────────────────
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def record(
|
def record(
|
||||||
region: Optional[str] = typer.Option(
|
region: Optional[str] = typer.Option(
|
||||||
@@ -231,10 +203,7 @@ def record(
|
|||||||
clipboard: bool = typer.Option(
|
clipboard: bool = typer.Option(
|
||||||
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
|
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
|
||||||
):
|
):
|
||||||
"""
|
"""Start or stop a screen recording with gpu-screen-recorder."""
|
||||||
Start or stop a screen recording with gpu-screen-recorder.
|
|
||||||
Running again stops the current recording.
|
|
||||||
"""
|
|
||||||
if pause:
|
if pause:
|
||||||
toggle_pause()
|
toggle_pause()
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user