Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad23da4eda | |||
| 06ebc4ffbf | |||
| f2f9fa1302 | |||
| b4020438f9 | |||
| 184ab20d11 | |||
| 5097e30a77 | |||
| ef71ae8afd | |||
| 6533533936 | |||
| 9688072e93 | |||
| 16e84ca998 | |||
| ba9926af18 | |||
| fd620e7487 | |||
| 0ec426e0f0 | |||
| ec5e6d3995 | |||
| 41a129bb90 |
+8
-1
@@ -31,6 +31,13 @@ if("shell" IN_LIST ENABLE_MODULES)
|
|||||||
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
|
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
|
||||||
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
|
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
|
||||||
endforeach()
|
endforeach()
|
||||||
install(FILES shell.qml DESTINATION "${INSTALL_QSCONFDIR}")
|
|
||||||
|
# Disable watching for changes
|
||||||
|
file(READ shell.qml SHELL_QML)
|
||||||
|
string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}")
|
||||||
|
file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}")
|
||||||
|
install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}")
|
||||||
|
|
||||||
|
# Greeter
|
||||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Text {
|
|||||||
color: DynamicColors.palette.m3onSurface
|
color: DynamicColors.palette.m3onSurface
|
||||||
font.family: Appearance.font.family.sans
|
font.family: Appearance.font.family.sans
|
||||||
font.pointSize: Appearance.font.size.normal
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
linkColor: DynamicColors.palette.m3onPrimaryFixedVariant
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -22,6 +22,7 @@ Singleton {
|
|||||||
property alias notifs: adapter.notifs
|
property alias notifs: adapter.notifs
|
||||||
property alias osd: adapter.osd
|
property alias osd: adapter.osd
|
||||||
property alias overview: adapter.overview
|
property alias overview: adapter.overview
|
||||||
|
property alias plugins: adapter.plugins
|
||||||
property bool recentlySaved: false
|
property bool recentlySaved: false
|
||||||
property alias screenshot: adapter.screenshot
|
property alias screenshot: adapter.screenshot
|
||||||
property alias services: adapter.services
|
property alias services: adapter.services
|
||||||
@@ -140,7 +141,8 @@ Singleton {
|
|||||||
launcher: serializeLauncher(),
|
launcher: serializeLauncher(),
|
||||||
colors: serializeColors(),
|
colors: serializeColors(),
|
||||||
dock: serializeDock(),
|
dock: serializeDock(),
|
||||||
screenshot: serializeScreenshot()
|
screenshot: serializeScreenshot(),
|
||||||
|
plugins: serializePlugins()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +291,13 @@ Singleton {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializePlugins(): var {
|
||||||
|
return {
|
||||||
|
enabled: plugins.enabled,
|
||||||
|
entries: plugins.entries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function serializeScreenshot(): var {
|
function serializeScreenshot(): var {
|
||||||
return {
|
return {
|
||||||
enable_pp: screenshot.enable_pp,
|
enable_pp: screenshot.enable_pp,
|
||||||
@@ -458,6 +467,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
property Overview overview: Overview {
|
property Overview overview: Overview {
|
||||||
}
|
}
|
||||||
|
property PluginConfig plugins: PluginConfig {
|
||||||
|
}
|
||||||
property Screenshot screenshot: Screenshot {
|
property Screenshot screenshot: Screenshot {
|
||||||
}
|
}
|
||||||
property Services services: Services {
|
property Services services: Services {
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ Singleton {
|
|||||||
readonly property alias wallLuminance: analyser.luminance
|
readonly property alias wallLuminance: analyser.luminance
|
||||||
|
|
||||||
function alterColor(c: color, a: real, layer: int): color {
|
function alterColor(c: color, a: real, layer: int): color {
|
||||||
const luminance = getLuminance(c);
|
const initLuminance = getLuminance(c);
|
||||||
|
const luminance = Math.max(initLuminance, 0.001);
|
||||||
|
|
||||||
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
|
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (0.2 + 0.3 * (1 - transparency.base)) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
|
||||||
const scale = (luminance + offset) / luminance;
|
const scale = (luminance + offset) / luminance;
|
||||||
const r = Math.max(0, Math.min(1, c.r * scale));
|
const r = Math.max(0, Math.min(1, c.r * scale));
|
||||||
const g = Math.max(0, Math.min(1, c.g * scale));
|
const g = Math.max(0, Math.min(1, c.g * scale));
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
JsonObject {
|
||||||
|
property bool enabled: false
|
||||||
|
property list<var> entries: [
|
||||||
|
{
|
||||||
|
id: "Plugin",
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import ZShell.Models
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias plugins: plugins.entries
|
||||||
|
|
||||||
|
FileSystemModel {
|
||||||
|
id: plugins
|
||||||
|
|
||||||
|
nameFilters: ["*.qml"]
|
||||||
|
path: Quickshell.env("HOME") + "/.config/zshell"
|
||||||
|
recursive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Models
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: FetchPlugins.plugins
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
required property FileSystemEntry modelData
|
||||||
|
|
||||||
|
activeAsync: Config.plugins.entries.some(p => {
|
||||||
|
return p.id === modelData.baseName && p.enabled;
|
||||||
|
})
|
||||||
|
source: modelData.path
|
||||||
|
}
|
||||||
|
}
|
||||||
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Quickshell
|
|
||||||
import "../scripts/fzf.js" as Fzf
|
import "../scripts/fzf.js" as Fzf
|
||||||
import "../scripts/fuzzysort.js" as Fuzzy
|
import "../scripts/fuzzysort.js" as Fuzzy
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
property var extraOpts: ({})
|
property var extraOpts: ({})
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ CustomRect {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
onLinkActivated: link => {
|
onLinkActivated: link => {
|
||||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
if (Config.launcher.uwsm)
|
||||||
|
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
||||||
|
else
|
||||||
|
Quickshell.execDetached(["xdg-open", link]);
|
||||||
root.visibilities.sidebar = false;
|
root.visibilities.sidebar = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -100,12 +100,14 @@ Item {
|
|||||||
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
||||||
value: root.brightness
|
value: root.brightness
|
||||||
|
|
||||||
onMoved: {
|
onPressedChanged: {
|
||||||
if (Config.osd.allMonBrightness) {
|
if (!pressed) {
|
||||||
root.monitor?.setBrightness(value);
|
if (Config.osd.allMonBrightness) {
|
||||||
} else {
|
for (const mon of Brightness.monitors) {
|
||||||
for (const mon of Brightness.monitors) {
|
mon.setBrightness(value);
|
||||||
mon.setBrightness(value);
|
}
|
||||||
|
} else {
|
||||||
|
root.monitor?.setBrightness(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ Item {
|
|||||||
key: "updates"
|
key: "updates"
|
||||||
name: "Updates"
|
name: "Updates"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListElement {
|
||||||
|
icon: "extension"
|
||||||
|
key: "plugins"
|
||||||
|
name: "Extensions"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Media update interval"
|
|
||||||
min: 0
|
min: 0
|
||||||
|
name: "Media update interval"
|
||||||
object: Config.dashboard
|
object: Config.dashboard
|
||||||
setting: "mediaUpdateInterval"
|
setting: "mediaUpdateInterval"
|
||||||
step: 50
|
step: 50
|
||||||
@@ -30,8 +30,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Resource update interval"
|
|
||||||
min: 0
|
min: 0
|
||||||
|
name: "Resource update interval"
|
||||||
object: Config.dashboard
|
object: Config.dashboard
|
||||||
setting: "resourceUpdateInterval"
|
setting: "resourceUpdateInterval"
|
||||||
step: 50
|
step: 50
|
||||||
@@ -41,8 +41,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Drag threshold"
|
|
||||||
min: 0
|
min: 0
|
||||||
|
name: "Drag threshold"
|
||||||
object: Config.dashboard
|
object: Config.dashboard
|
||||||
setting: "dragThreshold"
|
setting: "dragThreshold"
|
||||||
}
|
}
|
||||||
@@ -107,112 +107,112 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection {
|
// SettingsSection {
|
||||||
sectionId: "Layout Sizes"
|
// sectionId: "Layout Sizes"
|
||||||
|
//
|
||||||
SettingsHeader {
|
// SettingsHeader {
|
||||||
name: "Layout Sizes"
|
// name: "Layout Sizes"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Tab indicator height"
|
// name: "Tab indicator height"
|
||||||
value: String(Config.dashboard.sizes.tabIndicatorHeight)
|
// value: String(Config.dashboard.sizes.tabIndicatorHeight)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Tab indicator spacing"
|
// name: "Tab indicator spacing"
|
||||||
value: String(Config.dashboard.sizes.tabIndicatorSpacing)
|
// value: String(Config.dashboard.sizes.tabIndicatorSpacing)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Info width"
|
// name: "Info width"
|
||||||
value: String(Config.dashboard.sizes.infoWidth)
|
// value: String(Config.dashboard.sizes.infoWidth)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Info icon size"
|
// name: "Info icon size"
|
||||||
value: String(Config.dashboard.sizes.infoIconSize)
|
// value: String(Config.dashboard.sizes.infoIconSize)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Date time width"
|
// name: "Date time width"
|
||||||
value: String(Config.dashboard.sizes.dateTimeWidth)
|
// value: String(Config.dashboard.sizes.dateTimeWidth)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Media width"
|
// name: "Media width"
|
||||||
value: String(Config.dashboard.sizes.mediaWidth)
|
// value: String(Config.dashboard.sizes.mediaWidth)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Media progress sweep"
|
// name: "Media progress sweep"
|
||||||
value: String(Config.dashboard.sizes.mediaProgressSweep)
|
// value: String(Config.dashboard.sizes.mediaProgressSweep)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Media progress thickness"
|
// name: "Media progress thickness"
|
||||||
value: String(Config.dashboard.sizes.mediaProgressThickness)
|
// value: String(Config.dashboard.sizes.mediaProgressThickness)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Resource progress thickness"
|
// name: "Resource progress thickness"
|
||||||
value: String(Config.dashboard.sizes.resourceProgessThickness)
|
// value: String(Config.dashboard.sizes.resourceProgessThickness)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Weather width"
|
// name: "Weather width"
|
||||||
value: String(Config.dashboard.sizes.weatherWidth)
|
// value: String(Config.dashboard.sizes.weatherWidth)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Media cover art size"
|
// name: "Media cover art size"
|
||||||
value: String(Config.dashboard.sizes.mediaCoverArtSize)
|
// value: String(Config.dashboard.sizes.mediaCoverArtSize)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Media visualiser size"
|
// name: "Media visualiser size"
|
||||||
value: String(Config.dashboard.sizes.mediaVisualiserSize)
|
// value: String(Config.dashboard.sizes.mediaVisualiserSize)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingReadOnly {
|
// SettingReadOnly {
|
||||||
name: "Resource size"
|
// name: "Resource size"
|
||||||
value: String(Config.dashboard.sizes.resourceSize)
|
// value: String(Config.dashboard.sizes.resourceSize)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,18 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsSection {
|
||||||
|
sectionId: "Greeter"
|
||||||
|
|
||||||
|
SettingsHeader {
|
||||||
|
name: "Greeter"
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsIconButton {
|
||||||
|
name: "Install wallpaper and color scheme to greeter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
sectionId: "Idle"
|
sectionId: "Idle"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import qs.Modules.Settings.Controls
|
|||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool shouldBeActive: true
|
||||||
|
|
||||||
function addTimeoutEntry() {
|
function addTimeoutEntry() {
|
||||||
let list = [...Config.general.idle.timeouts];
|
let list = [...Config.general.idle.timeouts];
|
||||||
|
|
||||||
@@ -40,8 +42,26 @@ ColumnLayout {
|
|||||||
Config.save();
|
Config.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: shouldBeActive ? implicitHeight : 0
|
||||||
|
opacity: shouldBeActive ? 1 : 0
|
||||||
|
scale: shouldBeActive ? 1 : 0.8
|
||||||
spacing: Appearance.spacing.smaller
|
spacing: Appearance.spacing.smaller
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on y {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
name: "Idle Monitors"
|
name: "Idle Monitors"
|
||||||
@@ -52,6 +72,8 @@ ColumnLayout {
|
|||||||
|
|
||||||
SettingList {
|
SettingList {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
|
|
||||||
onAddActiveActionRequested: {
|
onAddActiveActionRequested: {
|
||||||
root.updateTimeoutEntry(index, "activeAction", "");
|
root.updateTimeoutEntry(index, "activeAction", "");
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import qs.Modules.Settings.Controls
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
SettingsSection {
|
||||||
|
sectionId: "Plugins"
|
||||||
|
|
||||||
|
SettingsHeader {
|
||||||
|
name: "Plugins"
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingBarEntryList {
|
||||||
|
name: "Enable or disable plugins"
|
||||||
|
object: Config.plugins
|
||||||
|
setting: "entries"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Max toasts"
|
|
||||||
min: 1
|
min: 1
|
||||||
|
name: "Max toasts"
|
||||||
object: Config.utilities
|
object: Config.utilities
|
||||||
setting: "maxToasts"
|
setting: "maxToasts"
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Panel width"
|
|
||||||
min: 1
|
min: 1
|
||||||
|
name: "Panel width"
|
||||||
object: Config.utilities.sizes
|
object: Config.utilities.sizes
|
||||||
setting: "width"
|
setting: "width"
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,8 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingSpinBox {
|
SettingSpinBox {
|
||||||
name: "Toast width"
|
|
||||||
min: 1
|
min: 1
|
||||||
|
name: "Toast width"
|
||||||
object: Config.utilities.sizes
|
object: Config.utilities.sizes
|
||||||
setting: "toastWidth"
|
setting: "toastWidth"
|
||||||
}
|
}
|
||||||
@@ -77,100 +77,100 @@ SettingsPage {
|
|||||||
setting: "gameModeChanged"
|
setting: "gameModeChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Do not disturb changed"
|
// name: "Do not disturb changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "dndChanged"
|
// setting: "dndChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Audio output changed"
|
// name: "Audio output changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "audioOutputChanged"
|
// setting: "audioOutputChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Audio input changed"
|
// name: "Audio input changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "audioInputChanged"
|
// setting: "audioInputChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Caps lock changed"
|
// name: "Caps lock changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "capsLockChanged"
|
// setting: "capsLockChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Num lock changed"
|
// name: "Num lock changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "numLockChanged"
|
// setting: "numLockChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Keyboard layout changed"
|
// name: "Keyboard layout changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "kbLayoutChanged"
|
// setting: "kbLayoutChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "VPN changed"
|
// name: "VPN changed"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "vpnChanged"
|
// setting: "vpnChanged"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Now playing"
|
// name: "Now playing"
|
||||||
object: Config.utilities.toasts
|
// object: Config.utilities.toasts
|
||||||
setting: "nowPlaying"
|
// setting: "nowPlaying"
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection {
|
// SettingsSection {
|
||||||
sectionId: "VPN"
|
// sectionId: "VPN"
|
||||||
|
//
|
||||||
SettingsHeader {
|
// SettingsHeader {
|
||||||
name: "VPN"
|
// name: "VPN"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingSwitch {
|
// SettingSwitch {
|
||||||
name: "Enable VPN integration"
|
// name: "Enable VPN integration"
|
||||||
object: Config.utilities.vpn
|
// object: Config.utilities.vpn
|
||||||
setting: "enabled"
|
// setting: "enabled"
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Separator {
|
// Separator {
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
SettingStringList {
|
// SettingStringList {
|
||||||
name: "Provider"
|
// name: "Provider"
|
||||||
addLabel: qsTr("Add VPN provider")
|
// addLabel: qsTr("Add VPN provider")
|
||||||
object: Config.utilities.vpn
|
// object: Config.utilities.vpn
|
||||||
setting: "provider"
|
// setting: "provider"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ Item {
|
|||||||
stack.push(screenshot);
|
stack.push(screenshot);
|
||||||
else if (currentCategory === "updates")
|
else if (currentCategory === "updates")
|
||||||
stack.push(updates);
|
stack.push(updates);
|
||||||
|
else if (currentCategory === "plugins")
|
||||||
|
stack.push(plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root
|
target: root
|
||||||
@@ -134,7 +136,7 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: searchBar.bottom
|
anchors.top: searchBar.bottom
|
||||||
anchors.topMargin: Appearance.spacing.smaller
|
anchors.topMargin: Appearance.spacing.smaller
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainerLowest
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
@@ -245,4 +247,11 @@ Item {
|
|||||||
Cat.SystemUpdates {
|
Cat.SystemUpdates {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: plugins
|
||||||
|
|
||||||
|
Cat.Plugins {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -207,6 +210,8 @@ ColumnLayout {
|
|||||||
StringListEditor {
|
StringListEditor {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
addLabel: qsTr("Add command argument")
|
addLabel: qsTr("Add command argument")
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
values: [...(modelData.command ?? [])]
|
values: [...(modelData.command ?? [])]
|
||||||
|
|
||||||
onListEdited: function (values) {
|
onListEdited: function (values) {
|
||||||
@@ -215,6 +220,9 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -233,6 +241,9 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import qs.Components
|
|||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
|
||||||
ColumnLayout {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
|
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
|
||||||
@@ -43,10 +43,9 @@ ColumnLayout {
|
|||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: shouldBeActive ? implicitHeight : 0
|
height: shouldBeActive ? layout.implicitHeight : 0
|
||||||
opacity: shouldBeActive ? 1 : 0
|
opacity: shouldBeActive ? 1 : 0
|
||||||
scale: shouldBeActive ? 1 : 0.8
|
scale: shouldBeActive ? 1 : 0.8
|
||||||
spacing: Appearance.spacing.smaller
|
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -77,115 +76,128 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
id: layout
|
||||||
font.pointSize: Appearance.font.size.larger
|
|
||||||
text: root.name
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
anchors.left: parent.left
|
||||||
model: [...root.object[root.setting]]
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Item {
|
spacing: Appearance.spacing.smaller
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: -(Appearance.spacing.smaller / 2)
|
|
||||||
color: DynamicColors.tPalette.m3outlineVariant
|
|
||||||
implicitHeight: 1
|
|
||||||
visible: index !== 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: layout
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: qsTr("From")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 33
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
|
|
||||||
CustomTextField {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Appearance.padding.normal
|
|
||||||
anchors.rightMargin: Appearance.padding.normal
|
|
||||||
text: modelData.from ?? ""
|
|
||||||
|
|
||||||
onEditingFinished: root.updateAlias(index, "from", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
icon: "delete"
|
|
||||||
type: IconButton.Tonal
|
|
||||||
|
|
||||||
onClicked: root.removeAlias(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: qsTr("To")
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 33
|
|
||||||
color: DynamicColors.tPalette.m3surface
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
CustomTextField {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Appearance.padding.normal
|
|
||||||
anchors.rightMargin: Appearance.padding.normal
|
|
||||||
text: modelData.to ?? ""
|
|
||||||
|
|
||||||
onEditingFinished: root.updateAlias(index, "to", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
icon: "add"
|
|
||||||
|
|
||||||
onClicked: root.addAlias()
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: qsTr("Add alias")
|
font.pointSize: Appearance.font.size.larger
|
||||||
|
text: root.name
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [...root.object[root.setting]]
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: -(Appearance.spacing.smaller / 2)
|
||||||
|
color: DynamicColors.tPalette.m3outlineVariant
|
||||||
|
implicitHeight: 1
|
||||||
|
visible: index !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.small
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("From")
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.preferredHeight: 33
|
||||||
|
Layout.preferredWidth: Math.max(Math.min(fromTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
CustomTextField {
|
||||||
|
id: fromTextField
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
|
||||||
|
text: modelData.from ?? ""
|
||||||
|
|
||||||
|
onEditingFinished: root.updateAlias(index, "from", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: "delete"
|
||||||
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
onClicked: root.removeAlias(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("To")
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.preferredHeight: 33
|
||||||
|
Layout.preferredWidth: Math.max(Math.min(toTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
CustomTextField {
|
||||||
|
id: toTextField
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
|
||||||
|
text: modelData.to ?? ""
|
||||||
|
|
||||||
|
onEditingFinished: root.updateAlias(index, "to", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
icon: "add"
|
||||||
|
|
||||||
|
onClicked: root.addAlias()
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Add alias")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -225,6 +228,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Separator {
|
Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: undefined
|
||||||
|
anchors.right: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Paths
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias button: iButton
|
||||||
|
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
|
||||||
|
required property string name
|
||||||
|
property bool shouldBeActive: true
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
|
||||||
|
opacity: shouldBeActive ? 1 : 0
|
||||||
|
scale: shouldBeActive ? 1 : 0.8
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on y {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -Appearance.padding.smaller
|
||||||
|
color: DynamicColors.palette.m3primaryContainer
|
||||||
|
opacity: root.highlighted ? 0.5 : 0
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.small
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: text
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pointSize: Appearance.font.size.larger
|
||||||
|
text: root.name
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: iButton
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||||
|
icon: "download"
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const lockBg = `${Paths.state}/lockscreen_bg.png`;
|
||||||
|
const scheme = `${Paths.state}/scheme.json`;
|
||||||
|
const face = `${Paths.home}/.face`;
|
||||||
|
const destination = "/etc/zshell-greeter/images";
|
||||||
|
Quickshell.execDetached(["pkexec", "sh", "-c", `mkdir -p ${destination}; cp ${lockBg} ${destination}; cp ${scheme} /etc/zshell-greeter; cp ${face} ${destination}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,473 +7,464 @@
|
|||||||
namespace ZShell::models {
|
namespace ZShell::models {
|
||||||
|
|
||||||
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
|
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_fileInfo(path)
|
, m_fileInfo(path)
|
||||||
, m_path(path)
|
, m_path(path)
|
||||||
, m_relativePath(relativePath)
|
, m_relativePath(relativePath)
|
||||||
, m_isImageInitialised(false)
|
, m_isImageInitialised(false)
|
||||||
, m_mimeTypeInitialised(false) {}
|
, m_mimeTypeInitialised(false) {
|
||||||
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::path() const {
|
QString FileSystemEntry::path() const {
|
||||||
return m_path;
|
return m_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
QString FileSystemEntry::relativePath() const {
|
QString FileSystemEntry::relativePath() const {
|
||||||
return m_relativePath;
|
return m_relativePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
QString FileSystemEntry::name() const {
|
QString FileSystemEntry::name() const {
|
||||||
return m_fileInfo.fileName();
|
return m_fileInfo.fileName();
|
||||||
};
|
};
|
||||||
|
|
||||||
QString FileSystemEntry::baseName() const {
|
QString FileSystemEntry::baseName() const {
|
||||||
return m_fileInfo.baseName();
|
return m_fileInfo.baseName();
|
||||||
};
|
};
|
||||||
|
|
||||||
QString FileSystemEntry::parentDir() const {
|
QString FileSystemEntry::parentDir() const {
|
||||||
return m_fileInfo.absolutePath();
|
return m_fileInfo.absolutePath();
|
||||||
};
|
};
|
||||||
|
|
||||||
QString FileSystemEntry::suffix() const {
|
QString FileSystemEntry::suffix() const {
|
||||||
return m_fileInfo.completeSuffix();
|
return m_fileInfo.completeSuffix();
|
||||||
};
|
};
|
||||||
|
|
||||||
qint64 FileSystemEntry::size() const {
|
qint64 FileSystemEntry::size() const {
|
||||||
return m_fileInfo.size();
|
return m_fileInfo.size();
|
||||||
};
|
};
|
||||||
|
|
||||||
bool FileSystemEntry::isDir() const {
|
bool FileSystemEntry::isDir() const {
|
||||||
return m_fileInfo.isDir();
|
return m_fileInfo.isDir();
|
||||||
};
|
};
|
||||||
|
|
||||||
bool FileSystemEntry::isImage() const {
|
bool FileSystemEntry::isImage() const {
|
||||||
if (!m_isImageInitialised) {
|
if (!m_isImageInitialised) {
|
||||||
QImageReader reader(m_path);
|
QImageReader reader(m_path);
|
||||||
m_isImage = reader.canRead();
|
m_isImage = reader.canRead();
|
||||||
m_isImageInitialised = true;
|
m_isImageInitialised = true;
|
||||||
}
|
}
|
||||||
return m_isImage;
|
return m_isImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::mimeType() const {
|
QString FileSystemEntry::mimeType() const {
|
||||||
if (!m_mimeTypeInitialised) {
|
if (!m_mimeTypeInitialised) {
|
||||||
const QMimeDatabase db;
|
static const QMimeDatabase s_db;
|
||||||
m_mimeType = db.mimeTypeForFile(m_path).name();
|
m_mimeType = s_db.mimeTypeForFile(m_path).name();
|
||||||
m_mimeTypeInitialised = true;
|
m_mimeTypeInitialised = true;
|
||||||
}
|
}
|
||||||
return m_mimeType;
|
return m_mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemEntry::updateRelativePath(const QDir& dir) {
|
void FileSystemEntry::updateRelativePath(const QDir& dir) {
|
||||||
const auto relPath = dir.relativeFilePath(m_path);
|
const auto relPath = dir.relativeFilePath(m_path);
|
||||||
if (m_relativePath != relPath) {
|
if (m_relativePath != relPath) {
|
||||||
m_relativePath = relPath;
|
m_relativePath = relPath;
|
||||||
emit relativePathChanged();
|
emit relativePathChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemModel::FileSystemModel(QObject* parent)
|
FileSystemModel::FileSystemModel(QObject* parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_recursive(false)
|
, m_recursive(false)
|
||||||
, m_watchChanges(true)
|
, m_watchChanges(true)
|
||||||
, m_showHidden(false)
|
, m_showHidden(false)
|
||||||
, m_filter(NoFilter) {
|
, m_filter(NoFilter) {
|
||||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
|
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
|
||||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
|
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
int FileSystemModel::rowCount(const QModelIndex& parent) const {
|
int FileSystemModel::rowCount(const QModelIndex& parent) const {
|
||||||
if (parent != QModelIndex()) {
|
if (parent != QModelIndex()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return static_cast<int>(m_entries.size());
|
return static_cast<int>(m_entries.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
|
QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
|
||||||
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
|
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
return QVariant::fromValue(m_entries.at(index.row()));
|
return QVariant::fromValue(m_entries.at(index.row()));
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> FileSystemModel::roleNames() const {
|
QHash<int, QByteArray> FileSystemModel::roleNames() const {
|
||||||
return { { Qt::UserRole, "modelData" } };
|
return { { Qt::UserRole, "modelData" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FileSystemModel::path() const {
|
QString FileSystemModel::path() const {
|
||||||
return m_path;
|
return m_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setPath(const QString& path) {
|
void FileSystemModel::setPath(const QString& path) {
|
||||||
if (m_path == path) {
|
if (m_path == path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_path = path;
|
m_path = path;
|
||||||
emit pathChanged();
|
emit pathChanged();
|
||||||
|
|
||||||
m_dir.setPath(m_path);
|
m_dir.setPath(m_path);
|
||||||
|
|
||||||
for (const auto& entry : std::as_const(m_entries)) {
|
for (const auto& entry : std::as_const(m_entries)) {
|
||||||
entry->updateRelativePath(m_dir);
|
entry->updateRelativePath(m_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemModel::recursive() const {
|
bool FileSystemModel::recursive() const {
|
||||||
return m_recursive;
|
return m_recursive;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setRecursive(bool recursive) {
|
void FileSystemModel::setRecursive(bool recursive) {
|
||||||
if (m_recursive == recursive) {
|
if (m_recursive == recursive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_recursive = recursive;
|
m_recursive = recursive;
|
||||||
emit recursiveChanged();
|
emit recursiveChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemModel::watchChanges() const {
|
bool FileSystemModel::watchChanges() const {
|
||||||
return m_watchChanges;
|
return m_watchChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setWatchChanges(bool watchChanges) {
|
void FileSystemModel::setWatchChanges(bool watchChanges) {
|
||||||
if (m_watchChanges == watchChanges) {
|
if (m_watchChanges == watchChanges) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_watchChanges = watchChanges;
|
m_watchChanges = watchChanges;
|
||||||
emit watchChangesChanged();
|
emit watchChangesChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemModel::showHidden() const {
|
bool FileSystemModel::showHidden() const {
|
||||||
return m_showHidden;
|
return m_showHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setShowHidden(bool showHidden) {
|
void FileSystemModel::setShowHidden(bool showHidden) {
|
||||||
if (m_showHidden == showHidden) {
|
if (m_showHidden == showHidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_showHidden = showHidden;
|
m_showHidden = showHidden;
|
||||||
emit showHiddenChanged();
|
emit showHiddenChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemModel::sortReverse() const {
|
bool FileSystemModel::sortReverse() const {
|
||||||
return m_sortReverse;
|
return m_sortReverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setSortReverse(bool sortReverse) {
|
void FileSystemModel::setSortReverse(bool sortReverse) {
|
||||||
if (m_sortReverse == sortReverse) {
|
if (m_sortReverse == sortReverse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_sortReverse = sortReverse;
|
m_sortReverse = sortReverse;
|
||||||
emit sortReverseChanged();
|
emit sortReverseChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemModel::Filter FileSystemModel::filter() const {
|
FileSystemModel::Filter FileSystemModel::filter() const {
|
||||||
return m_filter;
|
return m_filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setFilter(Filter filter) {
|
void FileSystemModel::setFilter(Filter filter) {
|
||||||
if (m_filter == filter) {
|
if (m_filter == filter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_filter = filter;
|
m_filter = filter;
|
||||||
emit filterChanged();
|
emit filterChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList FileSystemModel::nameFilters() const {
|
QStringList FileSystemModel::nameFilters() const {
|
||||||
return m_nameFilters;
|
return m_nameFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||||
if (m_nameFilters == nameFilters) {
|
if (m_nameFilters == nameFilters) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_nameFilters = nameFilters;
|
m_nameFilters = nameFilters;
|
||||||
emit nameFiltersChanged();
|
emit nameFiltersChanged();
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||||
if (m_recursive && m_watchChanges) {
|
if (m_recursive && m_watchChanges) {
|
||||||
const auto currentDir = m_dir;
|
const auto currentDir = m_dir;
|
||||||
const bool showHidden = m_showHidden;
|
const bool showHidden = m_showHidden;
|
||||||
const auto future = QtConcurrent::run([showHidden, path]() {
|
auto future = QtConcurrent::run([showHidden, path]() {
|
||||||
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||||
if (showHidden) {
|
if (showHidden) {
|
||||||
filters |= QDir::Hidden;
|
filters |= QDir::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDirIterator iter(path, filters, QDirIterator::Subdirectories);
|
QDirIterator iter(path, filters, QDirIterator::Subdirectories);
|
||||||
QStringList dirs;
|
QStringList dirs;
|
||||||
while (iter.hasNext()) {
|
while (iter.hasNext()) {
|
||||||
dirs << iter.next();
|
dirs << iter.next();
|
||||||
}
|
}
|
||||||
return dirs;
|
return dirs;
|
||||||
});
|
});
|
||||||
const auto watcher = new QFutureWatcher<QStringList>(this);
|
future.then(this, [currentDir, showHidden, this](const QStringList& paths) {
|
||||||
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
|
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
|
||||||
const auto paths = watcher->result();
|
// Ignore if dir or showHidden has changed
|
||||||
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
|
m_watcher.addPaths(paths);
|
||||||
// Ignore if dir or showHidden has changed
|
}
|
||||||
m_watcher.addPaths(paths);
|
});
|
||||||
}
|
}
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::update() {
|
void FileSystemModel::update() {
|
||||||
updateWatcher();
|
updateWatcher();
|
||||||
updateEntries();
|
updateEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::updateWatcher() {
|
void FileSystemModel::updateWatcher() {
|
||||||
if (!m_watcher.directories().isEmpty()) {
|
if (!m_watcher.directories().isEmpty()) {
|
||||||
m_watcher.removePaths(m_watcher.directories());
|
m_watcher.removePaths(m_watcher.directories());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_watchChanges || m_path.isEmpty()) {
|
if (!m_watchChanges || m_path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_watcher.addPath(m_path);
|
m_watcher.addPath(m_path);
|
||||||
watchDirIfRecursive(m_path);
|
watchDirIfRecursive(m_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::updateEntries() {
|
void FileSystemModel::updateEntries() {
|
||||||
if (m_path.isEmpty()) {
|
if (m_path.isEmpty()) {
|
||||||
if (!m_entries.isEmpty()) {
|
if (!m_entries.isEmpty()) {
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
qDeleteAll(m_entries);
|
qDeleteAll(m_entries);
|
||||||
m_entries.clear();
|
m_entries.clear();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
emit entriesChanged();
|
emit entriesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& future : m_futures) {
|
for (auto& future : m_futures) {
|
||||||
future.cancel();
|
future.cancel();
|
||||||
}
|
}
|
||||||
m_futures.clear();
|
m_futures.clear();
|
||||||
|
|
||||||
updateEntriesForDir(m_path);
|
updateEntriesForDir(m_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||||
const auto recursive = m_recursive;
|
const auto recursive = m_recursive;
|
||||||
const auto showHidden = m_showHidden;
|
const auto showHidden = m_showHidden;
|
||||||
const auto filter = m_filter;
|
const auto filter = m_filter;
|
||||||
const auto nameFilters = m_nameFilters;
|
const auto nameFilters = m_nameFilters;
|
||||||
|
|
||||||
QSet<QString> oldPaths;
|
QSet<QString> oldPaths;
|
||||||
for (const auto& entry : std::as_const(m_entries)) {
|
for (const auto& entry : std::as_const(m_entries)) {
|
||||||
oldPaths << entry->path();
|
oldPaths << entry->path();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString> > >& promise) {
|
||||||
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||||
|
|
||||||
std::optional<QDirIterator> iter;
|
std::optional<QDirIterator> iter;
|
||||||
|
|
||||||
if (filter == Images) {
|
if (filter == Images) {
|
||||||
QStringList extraNameFilters = nameFilters;
|
QStringList extraNameFilters = nameFilters;
|
||||||
const auto formats = QImageReader::supportedImageFormats();
|
const auto formats = QImageReader::supportedImageFormats();
|
||||||
for (const auto& format : formats) {
|
for (const auto& format : formats) {
|
||||||
extraNameFilters << "*." + format;
|
extraNameFilters << "*." + format;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir::Filters filters = QDir::Files;
|
QDir::Filters filters = QDir::Files;
|
||||||
if (showHidden) {
|
if (showHidden) {
|
||||||
filters |= QDir::Hidden;
|
filters |= QDir::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
iter.emplace(dir, extraNameFilters, filters, flags);
|
iter.emplace(dir, extraNameFilters, filters, flags);
|
||||||
} else {
|
} else {
|
||||||
QDir::Filters filters;
|
QDir::Filters filters;
|
||||||
|
|
||||||
if (filter == Files) {
|
if (filter == Files) {
|
||||||
filters = QDir::Files;
|
filters = QDir::Files;
|
||||||
} else if (filter == Dirs) {
|
} else if (filter == Dirs) {
|
||||||
filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||||
} else {
|
} else {
|
||||||
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
|
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showHidden) {
|
if (showHidden) {
|
||||||
filters |= QDir::Hidden;
|
filters |= QDir::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameFilters.isEmpty()) {
|
if (nameFilters.isEmpty()) {
|
||||||
iter.emplace(dir, filters, flags);
|
iter.emplace(dir, filters, flags);
|
||||||
} else {
|
} else {
|
||||||
iter.emplace(dir, nameFilters, filters, flags);
|
iter.emplace(dir, nameFilters, filters, flags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QSet<QString> newPaths;
|
QSet<QString> newPaths;
|
||||||
while (iter->hasNext()) {
|
while (iter->hasNext()) {
|
||||||
if (promise.isCanceled()) {
|
if (promise.isCanceled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString path = iter->next();
|
QString path = iter->next();
|
||||||
|
|
||||||
if (filter == Images) {
|
if (filter == Images) {
|
||||||
QImageReader reader(path);
|
QImageReader reader(path);
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newPaths.insert(path);
|
newPaths.insert(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise.isCanceled() || newPaths == oldPaths) {
|
if (promise.isCanceled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
|
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (m_futures.contains(dir)) {
|
if (m_futures.contains(dir)) {
|
||||||
m_futures[dir].cancel();
|
m_futures[dir].cancel();
|
||||||
}
|
}
|
||||||
m_futures.insert(dir, future);
|
m_futures.insert(dir, future);
|
||||||
|
|
||||||
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this);
|
future
|
||||||
|
.then(this,
|
||||||
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() {
|
[dir, this](QPair<QSet<QString>, QSet<QString> > result) {
|
||||||
m_futures.remove(dir);
|
m_futures.remove(dir);
|
||||||
|
if (!result.first.isEmpty() || !result.second.isEmpty()) {
|
||||||
if (!watcher->future().isResultReadyAt(0)) {
|
applyChanges(result.first, result.second);
|
||||||
watcher->deleteLater();
|
}
|
||||||
return;
|
})
|
||||||
}
|
.onCanceled(this, [dir, this]() {
|
||||||
|
m_futures.remove(dir);
|
||||||
const auto result = watcher->result();
|
});
|
||||||
applyChanges(result.first, result.second);
|
|
||||||
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher->setFuture(future);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
||||||
QList<int> removedIndices;
|
QList<int> removedIndices;
|
||||||
for (int i = 0; i < m_entries.size(); ++i) {
|
for (int i = 0; i < m_entries.size(); ++i) {
|
||||||
if (removedPaths.contains(m_entries[i]->path())) {
|
if (removedPaths.contains(m_entries[i]->path())) {
|
||||||
removedIndices << i;
|
removedIndices << i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||||
|
|
||||||
// Batch remove old entries
|
// Batch remove old entries
|
||||||
int start = -1;
|
int start = -1;
|
||||||
int end = -1;
|
int end = -1;
|
||||||
for (int idx : std::as_const(removedIndices)) {
|
for (int idx : std::as_const(removedIndices)) {
|
||||||
if (start == -1) {
|
if (start == -1) {
|
||||||
start = idx;
|
start = idx;
|
||||||
end = idx;
|
end = idx;
|
||||||
} else if (idx == end - 1) {
|
} else if (idx == end - 1) {
|
||||||
end = idx;
|
end = idx;
|
||||||
} else {
|
} else {
|
||||||
beginRemoveRows(QModelIndex(), end, start);
|
beginRemoveRows(QModelIndex(), end, start);
|
||||||
for (int i = start; i >= end; --i) {
|
for (int i = start; i >= end; --i) {
|
||||||
m_entries.takeAt(i)->deleteLater();
|
m_entries.takeAt(i)->deleteLater();
|
||||||
}
|
}
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
|
|
||||||
start = idx;
|
start = idx;
|
||||||
end = idx;
|
end = idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (start != -1) {
|
if (start != -1) {
|
||||||
beginRemoveRows(QModelIndex(), end, start);
|
beginRemoveRows(QModelIndex(), end, start);
|
||||||
for (int i = start; i >= end; --i) {
|
for (int i = start; i >= end; --i) {
|
||||||
m_entries.takeAt(i)->deleteLater();
|
m_entries.takeAt(i)->deleteLater();
|
||||||
}
|
}
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new entries
|
// Create new entries
|
||||||
QList<FileSystemEntry*> newEntries;
|
QList<FileSystemEntry*> newEntries;
|
||||||
for (const auto& path : addedPaths) {
|
for (const auto& path : addedPaths) {
|
||||||
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
}
|
}
|
||||||
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||||
return compareEntries(a, b);
|
return compareEntries(a, b);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch insert new entries
|
// Batch insert new entries
|
||||||
int insertStart = -1;
|
int insertStart = -1;
|
||||||
QList<FileSystemEntry*> batchItems;
|
QList<FileSystemEntry*> batchItems;
|
||||||
for (const auto& entry : std::as_const(newEntries)) {
|
for (const auto& entry : std::as_const(newEntries)) {
|
||||||
const auto it = std::lower_bound(
|
const auto it = std::lower_bound(
|
||||||
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||||
return compareEntries(a, b);
|
return compareEntries(a, b);
|
||||||
});
|
});
|
||||||
const auto row = static_cast<int>(it - m_entries.begin());
|
const auto row = static_cast<int>(it - m_entries.begin());
|
||||||
|
|
||||||
if (insertStart == -1) {
|
if (insertStart == -1) {
|
||||||
insertStart = row;
|
insertStart = row;
|
||||||
batchItems << entry;
|
batchItems << entry;
|
||||||
} else if (row == insertStart + batchItems.size()) {
|
} else if (row == insertStart + batchItems.size()) {
|
||||||
batchItems << entry;
|
batchItems << entry;
|
||||||
} else {
|
} else {
|
||||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||||
for (int i = 0; i < batchItems.size(); ++i) {
|
for (int i = 0; i < batchItems.size(); ++i) {
|
||||||
m_entries.insert(insertStart + i, batchItems[i]);
|
m_entries.insert(insertStart + i, batchItems[i]);
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
insertStart = row;
|
insertStart = row;
|
||||||
batchItems.clear();
|
batchItems.clear();
|
||||||
batchItems << entry;
|
batchItems << entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!batchItems.isEmpty()) {
|
if (!batchItems.isEmpty()) {
|
||||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||||
for (int i = 0; i < batchItems.size(); ++i) {
|
for (int i = 0; i < batchItems.size(); ++i) {
|
||||||
m_entries.insert(insertStart + i, batchItems[i]);
|
m_entries.insert(insertStart + i, batchItems[i]);
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit entriesChanged();
|
emit entriesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
|
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
|
||||||
if (a->isDir() != b->isDir()) {
|
if (a->isDir() != b->isDir()) {
|
||||||
return m_sortReverse ^ a->isDir();
|
return m_sortReverse ^ a->isDir();
|
||||||
}
|
}
|
||||||
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
|
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
|
||||||
return m_sortReverse ? cmp > 0 : cmp < 0;
|
return m_sortReverse ? cmp > 0 : cmp < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ZShell::models
|
} // namespace ZShell::models
|
||||||
|
|||||||
@@ -13,136 +13,136 @@
|
|||||||
namespace ZShell::models {
|
namespace ZShell::models {
|
||||||
|
|
||||||
class FileSystemEntry : public QObject {
|
class FileSystemEntry : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
|
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
|
||||||
|
|
||||||
Q_PROPERTY(QString path READ path CONSTANT)
|
Q_PROPERTY(QString path READ path CONSTANT)
|
||||||
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||||
Q_PROPERTY(QString name READ name CONSTANT)
|
Q_PROPERTY(QString name READ name CONSTANT)
|
||||||
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||||
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||||
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
|
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] QString path() const;
|
[[nodiscard]] QString path() const;
|
||||||
[[nodiscard]] QString relativePath() const;
|
[[nodiscard]] QString relativePath() const;
|
||||||
[[nodiscard]] QString name() const;
|
[[nodiscard]] QString name() const;
|
||||||
[[nodiscard]] QString baseName() const;
|
[[nodiscard]] QString baseName() const;
|
||||||
[[nodiscard]] QString parentDir() const;
|
[[nodiscard]] QString parentDir() const;
|
||||||
[[nodiscard]] QString suffix() const;
|
[[nodiscard]] QString suffix() const;
|
||||||
[[nodiscard]] qint64 size() const;
|
[[nodiscard]] qint64 size() const;
|
||||||
[[nodiscard]] bool isDir() const;
|
[[nodiscard]] bool isDir() const;
|
||||||
[[nodiscard]] bool isImage() const;
|
[[nodiscard]] bool isImage() const;
|
||||||
[[nodiscard]] QString mimeType() const;
|
[[nodiscard]] QString mimeType() const;
|
||||||
|
|
||||||
void updateRelativePath(const QDir& dir);
|
void updateRelativePath(const QDir& dir);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void relativePathChanged();
|
void relativePathChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QFileInfo m_fileInfo;
|
const QFileInfo m_fileInfo;
|
||||||
|
|
||||||
const QString m_path;
|
const QString m_path;
|
||||||
QString m_relativePath;
|
QString m_relativePath;
|
||||||
|
|
||||||
mutable bool m_isImage;
|
mutable bool m_isImage;
|
||||||
mutable bool m_isImageInitialised;
|
mutable bool m_isImageInitialised;
|
||||||
|
|
||||||
mutable QString m_mimeType;
|
mutable QString m_mimeType;
|
||||||
mutable bool m_mimeTypeInitialised;
|
mutable bool m_mimeTypeInitialised;
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileSystemModel : public QAbstractListModel {
|
class FileSystemModel : public QAbstractListModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||||
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
|
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
|
||||||
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
|
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
|
||||||
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
|
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
|
||||||
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
|
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
|
||||||
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
|
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
|
||||||
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
|
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
|
||||||
|
|
||||||
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
|
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Filter {
|
enum Filter {
|
||||||
NoFilter,
|
NoFilter,
|
||||||
Images,
|
Images,
|
||||||
Files,
|
Files,
|
||||||
Dirs
|
Dirs
|
||||||
};
|
};
|
||||||
Q_ENUM(Filter)
|
Q_ENUM(Filter)
|
||||||
|
|
||||||
explicit FileSystemModel(QObject* parent = nullptr);
|
explicit FileSystemModel(QObject* parent = nullptr);
|
||||||
|
|
||||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
[[nodiscard]] QString path() const;
|
[[nodiscard]] QString path() const;
|
||||||
void setPath(const QString& path);
|
void setPath(const QString& path);
|
||||||
|
|
||||||
[[nodiscard]] bool recursive() const;
|
[[nodiscard]] bool recursive() const;
|
||||||
void setRecursive(bool recursive);
|
void setRecursive(bool recursive);
|
||||||
|
|
||||||
[[nodiscard]] bool watchChanges() const;
|
[[nodiscard]] bool watchChanges() const;
|
||||||
void setWatchChanges(bool watchChanges);
|
void setWatchChanges(bool watchChanges);
|
||||||
|
|
||||||
[[nodiscard]] bool showHidden() const;
|
[[nodiscard]] bool showHidden() const;
|
||||||
void setShowHidden(bool showHidden);
|
void setShowHidden(bool showHidden);
|
||||||
|
|
||||||
[[nodiscard]] bool sortReverse() const;
|
[[nodiscard]] bool sortReverse() const;
|
||||||
void setSortReverse(bool sortReverse);
|
void setSortReverse(bool sortReverse);
|
||||||
|
|
||||||
[[nodiscard]] Filter filter() const;
|
[[nodiscard]] Filter filter() const;
|
||||||
void setFilter(Filter filter);
|
void setFilter(Filter filter);
|
||||||
|
|
||||||
[[nodiscard]] QStringList nameFilters() const;
|
[[nodiscard]] QStringList nameFilters() const;
|
||||||
void setNameFilters(const QStringList& nameFilters);
|
void setNameFilters(const QStringList& nameFilters);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void pathChanged();
|
void pathChanged();
|
||||||
void recursiveChanged();
|
void recursiveChanged();
|
||||||
void watchChangesChanged();
|
void watchChangesChanged();
|
||||||
void showHiddenChanged();
|
void showHiddenChanged();
|
||||||
void sortReverseChanged();
|
void sortReverseChanged();
|
||||||
void filterChanged();
|
void filterChanged();
|
||||||
void nameFiltersChanged();
|
void nameFiltersChanged();
|
||||||
void entriesChanged();
|
void entriesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir m_dir;
|
QDir m_dir;
|
||||||
QFileSystemWatcher m_watcher;
|
QFileSystemWatcher m_watcher;
|
||||||
QList<FileSystemEntry*> m_entries;
|
QList<FileSystemEntry*> m_entries;
|
||||||
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures;
|
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString> > > > m_futures;
|
||||||
|
|
||||||
QString m_path;
|
QString m_path;
|
||||||
bool m_recursive;
|
bool m_recursive;
|
||||||
bool m_watchChanges;
|
bool m_watchChanges;
|
||||||
bool m_showHidden;
|
bool m_showHidden;
|
||||||
bool m_sortReverse;
|
bool m_sortReverse = false;
|
||||||
Filter m_filter;
|
Filter m_filter;
|
||||||
QStringList m_nameFilters;
|
QStringList m_nameFilters;
|
||||||
|
|
||||||
void watchDirIfRecursive(const QString& path);
|
void watchDirIfRecursive(const QString& path);
|
||||||
void update();
|
void update();
|
||||||
void updateWatcher();
|
void updateWatcher();
|
||||||
void updateEntries();
|
void updateEntries();
|
||||||
void updateEntriesForDir(const QString& dir);
|
void updateEntriesForDir(const QString& dir);
|
||||||
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||||
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
|
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZShell::models
|
} // namespace ZShell::models
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"typer",
|
"typer",
|
||||||
"pillow",
|
"pillow",
|
||||||
|
"jinja2",
|
||||||
"materialyoucolor"
|
"materialyoucolor"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import typer
|
import typer
|
||||||
from zshell.subcommands import shell, scheme, screenshot, wallpaper
|
from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ app.add_typer(shell.app, name="shell")
|
|||||||
app.add_typer(scheme.app, name="scheme")
|
app.add_typer(scheme.app, name="scheme")
|
||||||
app.add_typer(screenshot.app, name="screenshot")
|
app.add_typer(screenshot.app, name="screenshot")
|
||||||
app.add_typer(wallpaper.app, name="wallpaper")
|
app.add_typer(wallpaper.app, name="wallpaper")
|
||||||
|
app.add_typer(record.app, name="record")
|
||||||
# app.add_typer(preset.app, name="preset")
|
# app.add_typer(preset.app, name="preset")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
RECORDER = "gpu-screen-recorder"
|
||||||
|
HOME = str(os.getenv("HOME", str(Path.home())))
|
||||||
|
CONFIG = Path(HOME) / ".config/zshell/config.json"
|
||||||
|
|
||||||
|
STATE_DIR = Path(HOME) / ".local/state/zshell/record"
|
||||||
|
TEMP_RECORDING = STATE_DIR / "recording.mp4"
|
||||||
|
REPLAY_RECORDING = STATE_DIR / "replay.mp4"
|
||||||
|
NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
|
||||||
|
|
||||||
|
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
|
||||||
|
str(Path(HOME) / "Videos/Recordings"))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_extra_args() -> list[str]:
|
||||||
|
try:
|
||||||
|
if CONFIG.is_file():
|
||||||
|
data = json.loads(CONFIG.read_text())
|
||||||
|
return data.get("record", {}).get("extraArgs", [])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _is_recording() -> bool:
|
||||||
|
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 5000) -> Optional[int]:
|
||||||
|
args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
|
||||||
|
if actions:
|
||||||
|
for action in actions:
|
||||||
|
args.extend(["-A", action])
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(args, capture_output=True, text=True)
|
||||||
|
return int(proc.stdout.strip()) if proc.stdout.strip().isdigit() else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _close_notification(notif_id: int):
|
||||||
|
subprocess.run(["notify-send", "--close", str(notif_id)],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_monitors() -> list[dict]:
|
||||||
|
try:
|
||||||
|
res = subprocess.run(["hyprctl", "monitors", "-j"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
return json.loads(res.stdout)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _focused_monitor_name() -> Optional[str]:
|
||||||
|
for m in _get_monitors():
|
||||||
|
if m.get("focused"):
|
||||||
|
return m["name"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _monitors_intersecting_region(x: int, y: int, w: int, h: int) -> list[dict]:
|
||||||
|
region = (x, y, x + w, y + h)
|
||||||
|
intersecting = []
|
||||||
|
for m in _get_monitors():
|
||||||
|
mx, my, mw, mh = m["x"], m["y"], m["width"], m["height"]
|
||||||
|
if not (region[2] <= mx or region[0] >= mx + mw or region[3] <= my or region[1] >= my + mh):
|
||||||
|
intersecting.append(m)
|
||||||
|
return intersecting
|
||||||
|
|
||||||
|
|
||||||
|
def _highest_refresh(monitors: list[dict]) -> float:
|
||||||
|
return max((m["refreshRate"] for m in monitors), default=60.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _slurp_region() -> Optional[str]:
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
|
||||||
|
import re
|
||||||
|
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
|
||||||
|
if match:
|
||||||
|
return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def start_recording(region: Optional[str], sound: bool):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
cmd = [RECORDER]
|
||||||
|
extra_args = _read_extra_args()
|
||||||
|
|
||||||
|
if region:
|
||||||
|
if region.lower() == "slurp" or not region:
|
||||||
|
geometry = _slurp_region()
|
||||||
|
if not geometry:
|
||||||
|
typer.echo("Region selection cancelled.")
|
||||||
|
raise typer.Abort()
|
||||||
|
else:
|
||||||
|
geometry = region
|
||||||
|
|
||||||
|
parsed = _parse_geometry(geometry)
|
||||||
|
if not parsed:
|
||||||
|
typer.echo("Invalid geometry format.")
|
||||||
|
raise typer.Abort()
|
||||||
|
x, y, w, h = parsed
|
||||||
|
|
||||||
|
monitors = _monitors_intersecting_region(x, y, w, h)
|
||||||
|
framerate = _highest_refresh(monitors)
|
||||||
|
cmd.extend(["-w", "region", "-region", geometry, "-f", str(int(framerate))])
|
||||||
|
|
||||||
|
else:
|
||||||
|
monitor_name = _focused_monitor_name()
|
||||||
|
if not monitor_name:
|
||||||
|
typer.echo("No focused monitor found.")
|
||||||
|
raise typer.Abort()
|
||||||
|
|
||||||
|
monitors = _get_monitors()
|
||||||
|
mon = next((m for m in monitors if m["name"] == monitor_name), None)
|
||||||
|
rate = int(mon["refreshRate"]) if mon else 60
|
||||||
|
cmd.extend(["-w", monitor_name, "-f", str(rate)])
|
||||||
|
|
||||||
|
if sound:
|
||||||
|
cmd.extend(["-a", "default_output"])
|
||||||
|
|
||||||
|
cmd.extend(extra_args)
|
||||||
|
cmd.extend(["-o", str(TEMP_RECORDING)])
|
||||||
|
|
||||||
|
subprocess.Popen(cmd, start_new_session=True,
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
|
||||||
|
if notif_id is not None:
|
||||||
|
NOTIF_ID_FILE.write_text(str(notif_id))
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
if not _is_recording():
|
||||||
|
_notify("Recording failed",
|
||||||
|
"Check gpu-screen-recorder output.", timeout=5000)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_recording(clipboard: bool):
|
||||||
|
subprocess.run(["pkill", "-f", RECORDER],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
for _ in range(50):
|
||||||
|
if not _is_recording():
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
dest_dir = Path(RECORDINGS_DIR)
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
final_path = dest_dir / f"recording_{timestamp}.mp4"
|
||||||
|
|
||||||
|
if TEMP_RECORDING.exists():
|
||||||
|
TEMP_RECORDING.rename(final_path)
|
||||||
|
|
||||||
|
if NOTIF_ID_FILE.is_file():
|
||||||
|
try:
|
||||||
|
_close_notification(int(NOTIF_ID_FILE.read_text().strip()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
NOTIF_ID_FILE.unlink()
|
||||||
|
|
||||||
|
if clipboard:
|
||||||
|
subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_pause():
|
||||||
|
subprocess.run(["pkill", "-USR2", "-f", RECORDER],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
typer.echo("Toggled pause.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def record(
|
||||||
|
region: Optional[str] = typer.Option(
|
||||||
|
None, "--region", "-r",
|
||||||
|
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
|
||||||
|
),
|
||||||
|
sound: bool = typer.Option(
|
||||||
|
False, "--sound", "-s", help="Record audio from default output."),
|
||||||
|
pause: bool = typer.Option(
|
||||||
|
False, "--pause", "-p", help="Toggle pause/resume."),
|
||||||
|
clipboard: bool = typer.Option(
|
||||||
|
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
|
||||||
|
):
|
||||||
|
"""Start or stop a screen recording with gpu-screen-recorder."""
|
||||||
|
if pause:
|
||||||
|
toggle_pause()
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
if _is_recording():
|
||||||
|
stop_recording(clipboard)
|
||||||
|
else:
|
||||||
|
start_recording(region, sound)
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
//@ pragma UseQApplication
|
//@ pragma UseQApplication
|
||||||
//@ pragma Env QSG_RENDER_LOOP=threaded
|
//@ pragma Env QSG_RENDER_LOOP=threaded
|
||||||
// @ pragma Env QSG_RHI_BACKEND=vulkan
|
//@ pragma Env QSG_RHI_BACKEND=vulkan
|
||||||
//@ pragma Env QSG_NO_VSYNC=1
|
//@ pragma Env QSG_NO_VSYNC=1
|
||||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||||
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round
|
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round
|
||||||
//@ pragma DropExpensiveFonts
|
//@ pragma DropExpensiveFonts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Extensions
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.Wallpaper
|
import qs.Modules.Wallpaper
|
||||||
import qs.Modules.Lock
|
import qs.Modules.Lock
|
||||||
@@ -14,6 +15,8 @@ import qs.Helpers
|
|||||||
import qs.Modules.Polkit
|
import qs.Modules.Polkit
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
|
settings.watchFiles: true
|
||||||
|
|
||||||
Windows {
|
Windows {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,4 +39,7 @@ ShellRoot {
|
|||||||
|
|
||||||
Polkit {
|
Polkit {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadExtensions {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user