summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml41
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml102
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml102
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml344
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml40
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml296
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml820
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml1707
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml46
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml73
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml405
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml200
12 files changed, 4176 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml
new file mode 100644
index 0000000..639389c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml
@@ -0,0 +1,41 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+DankActionButton {
+ id: customButtonKeyboard
+ circular: false
+ property string text: ""
+ width: 40
+ height: 40
+ property bool isShift: false
+ color: Theme.surface
+
+ property bool isIcon: text === "keyboard_hide" || text === "Backspace" || text === "Enter"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: {
+ if (parent.text === "keyboard_hide") return "keyboard_hide"
+ if (parent.text === "Backspace") return "backspace"
+ if (parent.text === "Enter") return "keyboard_return"
+ return ""
+ }
+ size: 20
+ color: Theme.surfaceText
+ visible: parent.isIcon
+ }
+
+ StyledText {
+ id: contentItem
+ anchors.centerIn: parent
+ text: parent.text
+ color: Theme.surfaceText
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Normal
+ visible: !parent.isIcon
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml
new file mode 100644
index 0000000..0486890
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml
@@ -0,0 +1,102 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+
+PanelWindow {
+ id: root
+
+ property bool active: false
+
+ signal fadeCompleted
+ signal fadeCancelled
+
+ visible: active
+ color: "transparent"
+
+ WlrLayershell.namespace: "dms:fade-to-dpms"
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
+
+ anchors {
+ left: true
+ right: true
+ top: true
+ bottom: true
+ }
+
+ Rectangle {
+ id: fadeOverlay
+ anchors.fill: parent
+ color: "black"
+ opacity: 0
+
+ onOpacityChanged: {
+ if (opacity >= 0.99 && root.active) {
+ root.fadeCompleted();
+ }
+ }
+ }
+
+ SequentialAnimation {
+ id: fadeSeq
+ running: false
+
+ NumberAnimation {
+ target: fadeOverlay
+ property: "opacity"
+ from: 0.0
+ to: 1.0
+ duration: SettingsData.fadeToDpmsGracePeriod * 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ function startFade() {
+ if (!SettingsData.fadeToDpmsEnabled)
+ return;
+ active = true;
+ fadeOverlay.opacity = 0.0;
+ fadeSeq.stop();
+ fadeSeq.start();
+ }
+
+ function cancelFade() {
+ fadeSeq.stop();
+ fadeOverlay.opacity = 0.0;
+ active = false;
+ fadeCancelled();
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.active
+ onClicked: root.cancelFade()
+ onPressed: root.cancelFade()
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: root.active
+
+ Keys.onPressed: event => {
+ root.cancelFade();
+ event.accepted = true;
+ }
+ }
+
+ Component.onCompleted: {
+ if (active) {
+ forceActiveFocus();
+ }
+ }
+
+ onActiveChanged: {
+ if (active) {
+ forceActiveFocus();
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml
new file mode 100644
index 0000000..c73a59a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml
@@ -0,0 +1,102 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+
+PanelWindow {
+ id: root
+
+ property bool active: false
+
+ signal fadeCompleted
+ signal fadeCancelled
+
+ visible: active
+ color: "transparent"
+
+ WlrLayershell.namespace: "dms:fade-to-lock"
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
+
+ anchors {
+ left: true
+ right: true
+ top: true
+ bottom: true
+ }
+
+ Rectangle {
+ id: fadeOverlay
+ anchors.fill: parent
+ color: "black"
+ opacity: 0
+
+ onOpacityChanged: {
+ if (opacity >= 0.99 && root.active) {
+ root.fadeCompleted();
+ }
+ }
+ }
+
+ SequentialAnimation {
+ id: fadeSeq
+ running: false
+
+ NumberAnimation {
+ target: fadeOverlay
+ property: "opacity"
+ from: 0.0
+ to: 1.0
+ duration: SettingsData.fadeToLockGracePeriod * 1000
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ function startFade() {
+ if (!SettingsData.fadeToLockEnabled)
+ return;
+ active = true;
+ fadeOverlay.opacity = 0.0;
+ fadeSeq.stop();
+ fadeSeq.start();
+ }
+
+ function cancelFade() {
+ fadeSeq.stop();
+ fadeOverlay.opacity = 0.0;
+ active = false;
+ fadeCancelled();
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.active
+ onClicked: root.cancelFade()
+ onPressed: root.cancelFade()
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: root.active
+
+ Keys.onPressed: event => {
+ root.cancelFade();
+ event.accepted = true;
+ }
+ }
+
+ Component.onCompleted: {
+ if (active) {
+ forceActiveFocus();
+ }
+ }
+
+ onActiveChanged: {
+ if (active) {
+ forceActiveFocus();
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml
new file mode 100644
index 0000000..cfc0c61
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml
@@ -0,0 +1,344 @@
+import QtQuick
+import qs.Common
+
+Rectangle {
+ id: root
+ property Item target
+ height: 60 * 5
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ color: Theme.surfaceContainerHigh
+
+ signal dismissed
+
+ property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
+ property double columnSpacing: 0.02 * height // vertical spacing between keyboard
+ property bool shift: false //Boolean for the shift state
+ property bool symbols: false //Boolean for the symbol state
+ property double columns: 10 // Number of column
+ property double rows: 4 // Number of row
+
+ property string strShift: '\u2191'
+ property string strBackspace: "Backspace"
+ property string strEnter: "Enter"
+ property string strClose: "keyboard_hide"
+
+ property var modelKeyboard: {
+ "row_1": [{
+ "text": 'q',
+ "symbol": '1',
+ "width": 1
+ }, {
+ "text": 'w',
+ "symbol": '2',
+ "width": 1
+ }, {
+ "text": 'e',
+ "symbol": '3',
+ "width": 1
+ }, {
+ "text": 'r',
+ "symbol": '4',
+ "width": 1
+ }, {
+ "text": 't',
+ "symbol": '5',
+ "width": 1
+ }, {
+ "text": 'y',
+ "symbol": '6',
+ "width": 1
+ }, {
+ "text": 'u',
+ "symbol": '7',
+ "width": 1
+ }, {
+ "text": 'i',
+ "symbol": '8',
+ "width": 1
+ }, {
+ "text": 'o',
+ "symbol": '9',
+ "width": 1
+ }, {
+ "text": 'p',
+ "symbol": '0',
+ "width": 1
+ }],
+ "row_2": [{
+ "text": 'a',
+ "symbol": '-',
+ "width": 1
+ }, {
+ "text": 's',
+ "symbol": '/',
+ "width": 1
+ }, {
+ "text": 'd',
+ "symbol": ':',
+ "width": 1
+ }, {
+ "text": 'f',
+ "symbol": ';',
+ "width": 1
+ }, {
+ "text": 'g',
+ "symbol": '(',
+ "width": 1
+ }, {
+ "text": 'h',
+ "symbol": ')',
+ "width": 1
+ }, {
+ "text": 'j',
+ "symbol": '€',
+ "width": 1
+ }, {
+ "text": 'k',
+ "symbol": '&',
+ "width": 1
+ }, {
+ "text": 'l',
+ "symbol": '@',
+ "width": 1
+ }],
+ "row_3": [{
+ "text": strShift,
+ "symbol": strShift,
+ "width": 1.5
+ }, {
+ "text": 'z',
+ "symbol": '.',
+ "width": 1
+ }, {
+ "text": 'x',
+ "symbol": ',',
+ "width": 1
+ }, {
+ "text": 'c',
+ "symbol": '?',
+ "width": 1
+ }, {
+ "text": 'v',
+ "symbol": '!',
+ "width": 1
+ }, {
+ "text": 'b',
+ "symbol": "'",
+ "width": 1
+ }, {
+ "text": 'n',
+ "symbol": "%",
+ "width": 1
+ }, {
+ "text": 'm',
+ "symbol": '"',
+ "width": 1
+ }, {
+ "text": strBackspace,
+ "symbol": strBackspace,
+ "width": 1.5
+ }],
+ "row_4": [{
+ "text": strClose,
+ "symbol": strClose,
+ "width": 1.5
+ }, {
+ "text": "123",
+ "symbol": 'ABC',
+ "width": 1.5
+ }, {
+ "text": ' ',
+ "symbol": ' ',
+ "width": 4.5
+ }, {
+ "text": '.',
+ "symbol": '.',
+ "width": 1
+ }, {
+ "text": strEnter,
+ "symbol": strEnter,
+ "width": 1.5
+ }]
+ }
+
+ //Here is the corresponding table between the ascii and the key event
+ property var tableKeyEvent: {
+ "_0": Qt.Key_0,
+ "_1": Qt.Key_1,
+ "_2": Qt.Key_2,
+ "_3": Qt.Key_3,
+ "_4": Qt.Key_4,
+ "_5": Qt.Key_5,
+ "_6": Qt.Key_6,
+ "_7": Qt.Key_7,
+ "_8": Qt.Key_8,
+ "_9": Qt.Key_9,
+ "_a": Qt.Key_A,
+ "_b": Qt.Key_B,
+ "_c": Qt.Key_C,
+ "_d": Qt.Key_D,
+ "_e": Qt.Key_E,
+ "_f": Qt.Key_F,
+ "_g": Qt.Key_G,
+ "_h": Qt.Key_H,
+ "_i": Qt.Key_I,
+ "_j": Qt.Key_J,
+ "_k": Qt.Key_K,
+ "_l": Qt.Key_L,
+ "_m": Qt.Key_M,
+ "_n": Qt.Key_N,
+ "_o": Qt.Key_O,
+ "_p": Qt.Key_P,
+ "_q": Qt.Key_Q,
+ "_r": Qt.Key_R,
+ "_s": Qt.Key_S,
+ "_t": Qt.Key_T,
+ "_u": Qt.Key_U,
+ "_v": Qt.Key_V,
+ "_w": Qt.Key_W,
+ "_x": Qt.Key_X,
+ "_y": Qt.Key_Y,
+ "_z": Qt.Key_Z,
+ "_←": Qt.Key_Backspace,
+ "_return": Qt.Key_Return,
+ "_ ": Qt.Key_Space,
+ "_-": Qt.Key_Minus,
+ "_/": Qt.Key_Slash,
+ "_:": Qt.Key_Colon,
+ "_;": Qt.Key_Semicolon,
+ "_(": Qt.Key_BracketLeft,
+ "_)": Qt.Key_BracketRight,
+ "_€": parseInt(
+ "20ac",
+ 16) // I didn't find the appropriate Qt event so I used the hex format
+ ,
+ "_&": Qt.Key_Ampersand,
+ "_@": Qt.Key_At,
+ '_"': Qt.Key_QuoteDbl,
+ "_.": Qt.Key_Period,
+ "_,": Qt.Key_Comma,
+ "_?": Qt.Key_Question,
+ "_!": Qt.Key_Exclam,
+ "_'": Qt.Key_Apostrophe,
+ "_%": Qt.Key_Percent,
+ "_*": Qt.Key_Asterisk
+}
+
+Item {
+id: keyboard_container
+anchors.left: parent.left
+anchors.leftMargin: 5
+anchors.right: parent.right
+anchors.top: parent.top
+anchors.topMargin: 5
+anchors.bottom: parent.bottom
+anchors.bottomMargin: 5
+
+//One column which contains 5 rows
+Column {
+spacing: columnSpacing
+
+Row {
+id: row_1
+spacing: rowSpacing
+Repeater {
+model: modelKeyboard["row_1"]
+delegate: CustomButtonKeyboard {
+text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
+() : modelData.text
+width: modelData.width * keyboard_container.width / columns - rowSpacing
+height: keyboard_container.height / rows - columnSpacing
+
+onClicked: root.clicked(text)
+}
+}
+}
+Row {
+id: row_2
+spacing: rowSpacing
+Repeater {
+model: modelKeyboard["row_2"]
+delegate: CustomButtonKeyboard {
+text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
+() : modelData.text
+width: modelData.width * keyboard_container.width / columns - rowSpacing
+height: keyboard_container.height / rows - columnSpacing
+
+onClicked: root.clicked(text)
+}
+}
+}
+Row {
+id: row_3
+spacing: rowSpacing
+Repeater {
+model: modelKeyboard["row_3"]
+delegate: CustomButtonKeyboard {
+text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
+() : modelData.text
+width: modelData.width * keyboard_container.width / columns - rowSpacing
+height: keyboard_container.height / rows - columnSpacing
+isShift: shift && text === strShift
+
+onClicked: root.clicked(text)
+}
+}
+}
+Row {
+id: row_4
+spacing: rowSpacing
+Repeater {
+model: modelKeyboard["row_4"]
+delegate: CustomButtonKeyboard {
+text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
+() : modelData.text
+width: modelData.width * keyboard_container.width / columns - rowSpacing
+height: keyboard_container.height / rows - columnSpacing
+
+onClicked: root.clicked(text)
+}
+}
+}
+}
+}
+signal clicked(string text)
+
+Connections {
+target: root
+function onClicked(text) {
+if (!keyboard_controller.target)
+return
+if (text === strShift) {
+root.shift = !root.shift
+} else if (text === '123') {
+root.symbols = true
+} else if (text === 'ABC') {
+root.symbols = false
+} else if (text === strEnter) {
+if (keyboard_controller.target.accepted) {
+keyboard_controller.target.accepted()
+}
+} else if (text === strClose) {
+root.dismissed()
+} else {
+if (text === strBackspace) {
+var current = keyboard_controller.target.text
+keyboard_controller.target.text = current.slice(0, current.length - 1)
+} else {
+var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase
+() : text)
+var current = keyboard_controller.target.text
+var cursorPos = keyboard_controller.target.cursorPosition
+keyboard_controller.target.text = current.slice(0,
+cursorPos) + charToInsert + current.slice(cursorPos)
+keyboard_controller.target.cursorPosition = cursorPos + 1
+}
+
+if (root.shift && text !== strShift)
+root.shift = false
+}
+}
+}
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml
new file mode 100644
index 0000000..09d77a5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml
@@ -0,0 +1,40 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+
+Item {
+ id: keyboard_controller
+
+ // reference on the TextInput
+ property Item target
+ //Booléan on the state of the keyboard
+ property bool isKeyboardActive: false
+
+ property var rootObject
+
+ function show() {
+ if (!isKeyboardActive && keyboard === null) {
+ keyboard = keyboardComponent.createObject(
+ keyboard_controller.rootObject)
+ keyboard.target = keyboard_controller.target
+ keyboard.dismissed.connect(hide)
+ isKeyboardActive = true
+ } else
+ console.log("The keyboard is already shown")
+ }
+
+ function hide() {
+ if (isKeyboardActive && keyboard !== null) {
+ keyboard.destroy()
+ isKeyboardActive = false
+ } else
+ console.log("The keyboard is already hidden")
+ }
+
+ // private
+ property Item keyboard: null
+ Component {
+ id: keyboardComponent
+ Keyboard {}
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml
new file mode 100644
index 0000000..9a5a00f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml
@@ -0,0 +1,296 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+
+Scope {
+ id: root
+
+ property string sharedPasswordBuffer: ""
+ property bool shouldLock: false
+
+ onShouldLockChanged: {
+ IdleService.isShellLocked = shouldLock;
+ if (shouldLock && lockPowerOffArmed) {
+ lockStateCheck.restart();
+ }
+ }
+
+ Timer {
+ id: lockStateCheck
+ interval: 100
+ repeat: false
+ onTriggered: {
+ if (sessionLock.locked && lockPowerOffArmed) {
+ pendingLock = false;
+ IdleService.monitorsOff = true;
+ CompositorService.powerOffMonitors();
+ lockWakeAllowed = false;
+ lockWakeDebounce.restart();
+ lockPowerOffArmed = false;
+ dpmsReapplyTimer.start();
+ }
+ }
+ }
+
+ property bool lockInitiatedLocally: false
+ property bool pendingLock: false
+ property bool lockPowerOffArmed: false
+ property bool lockWakeAllowed: false
+
+ Component.onCompleted: {
+ IdleService.lockComponent = this;
+ if (SettingsData.lockAtStartup)
+ lock();
+ }
+
+ function notifyLoginctl(lockAction: bool) {
+ if (!SettingsData.loginctlLockIntegration || !DMSService.isConnected)
+ return;
+ if (lockAction)
+ DMSService.lockSession(() => {});
+ else
+ DMSService.unlockSession(() => {});
+ }
+
+ function lock() {
+ if (SettingsData.customPowerActionLock?.length > 0) {
+ Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLock]);
+ return;
+ }
+ if (shouldLock || pendingLock)
+ return;
+
+ lockInitiatedLocally = true;
+ lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
+
+ if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
+ pendingLock = true;
+ notifyLoginctl(true);
+ return;
+ }
+
+ shouldLock = true;
+ notifyLoginctl(true);
+ }
+
+ function unlock() {
+ if (!shouldLock)
+ return;
+ lockInitiatedLocally = false;
+ notifyLoginctl(false);
+ shouldLock = false;
+ }
+
+ function forceReset() {
+ lockInitiatedLocally = false;
+ pendingLock = false;
+ shouldLock = false;
+ }
+
+ function activate() {
+ lock();
+ }
+
+ Connections {
+ target: SessionService
+
+ function onSessionLocked() {
+ if (shouldLock || pendingLock)
+ return;
+ if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
+ pendingLock = true;
+ lockInitiatedLocally = false;
+ return;
+ }
+ lockInitiatedLocally = false;
+ lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
+ shouldLock = true;
+ }
+
+ function onSessionUnlocked() {
+ if (pendingLock) {
+ pendingLock = false;
+ lockInitiatedLocally = false;
+ return;
+ }
+ if (!shouldLock || lockInitiatedLocally)
+ return;
+ shouldLock = false;
+ }
+
+ function onLoginctlStateChanged() {
+ if (SessionService.active && pendingLock) {
+ pendingLock = false;
+ lockInitiatedLocally = true;
+ lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
+ shouldLock = true;
+ return;
+ }
+ if (SessionService.locked && !shouldLock && !pendingLock) {
+ lockInitiatedLocally = false;
+ lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
+ shouldLock = true;
+ }
+ }
+ }
+
+ Connections {
+ target: IdleService
+
+ function onLockRequested() {
+ lock();
+ }
+ }
+
+ WlSessionLock {
+ id: sessionLock
+
+ locked: shouldLock
+
+ WlSessionLockSurface {
+ id: lockSurface
+
+ property string currentScreenName: screen?.name ?? ""
+ property bool isActiveScreen: {
+ if (Quickshell.screens.length <= 1)
+ return true;
+ if (SettingsData.lockScreenActiveMonitor === "all")
+ return true;
+ return currentScreenName === SettingsData.lockScreenActiveMonitor;
+ }
+
+ color: isActiveScreen ? "transparent" : SettingsData.lockScreenInactiveColor
+
+ LockSurface {
+ anchors.fill: parent
+ visible: lockSurface.isActiveScreen
+ lock: sessionLock
+ sharedPasswordBuffer: root.sharedPasswordBuffer
+ screenName: lockSurface.currentScreenName
+ isLocked: shouldLock
+ onUnlockRequested: root.unlock()
+ onPasswordChanged: newPassword => {
+ root.sharedPasswordBuffer = newPassword;
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: sessionLock
+
+ function onLockedChanged() {
+ if (sessionLock.locked) {
+ pendingLock = false;
+ if (lockPowerOffArmed && SettingsData.lockScreenPowerOffMonitorsOnLock) {
+ IdleService.monitorsOff = true;
+ CompositorService.powerOffMonitors();
+ lockWakeAllowed = false;
+ lockWakeDebounce.restart();
+ }
+ lockPowerOffArmed = false;
+ dpmsReapplyTimer.start();
+ return;
+ }
+
+ lockWakeAllowed = false;
+ if (IdleService.monitorsOff && SettingsData.lockScreenPowerOffMonitorsOnLock) {
+ IdleService.monitorsOff = false;
+ CompositorService.powerOnMonitors();
+ }
+ }
+ }
+
+ LockScreenDemo {
+ id: demoWindow
+ }
+
+ IpcHandler {
+ target: "lock"
+
+ function lock() {
+ root.lock();
+ }
+
+ function unlock() {
+ root.unlock();
+ }
+
+ function forceReset() {
+ root.forceReset();
+ }
+
+ function demo() {
+ demoWindow.showDemo();
+ }
+
+ function isLocked(): bool {
+ return sessionLock.locked;
+ }
+
+ function status(): string {
+ return JSON.stringify({
+ shouldLock: root.shouldLock,
+ sessionLockLocked: sessionLock.locked,
+ lockInitiatedLocally: root.lockInitiatedLocally,
+ pendingLock: root.pendingLock,
+ loginctlLocked: SessionService.locked,
+ loginctlActive: SessionService.active
+ });
+ }
+ }
+
+ Timer {
+ id: dpmsReapplyTimer
+ interval: 100
+ repeat: false
+ onTriggered: IdleService.reapplyDpmsIfNeeded()
+ }
+
+ Timer {
+ id: lockWakeDebounce
+ interval: 200
+ repeat: false
+ onTriggered: {
+ if (!sessionLock.locked)
+ return;
+ if (!SettingsData.lockScreenPowerOffMonitorsOnLock)
+ return;
+ if (!IdleService.monitorsOff) {
+ lockWakeAllowed = true;
+ return;
+ }
+ if (lockWakeAllowed) {
+ IdleService.monitorsOff = false;
+ CompositorService.powerOnMonitors();
+ } else {
+ lockWakeAllowed = true;
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: sessionLock.locked
+ hoverEnabled: enabled
+ onPressed: lockWakeDebounce.restart()
+ onPositionChanged: lockWakeDebounce.restart()
+ onWheel: lockWakeDebounce.restart()
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: sessionLock.locked
+
+ Keys.onPressed: event => {
+ if (!sessionLock.locked)
+ return;
+ lockWakeDebounce.restart();
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml
new file mode 100644
index 0000000..0a0ee84
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml
@@ -0,0 +1,820 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property bool isVisible: false
+ property bool showLogout: true
+ property int selectedIndex: 0
+ property int selectedRow: 0
+ property int selectedCol: 0
+ property var visibleActions: []
+ property int gridColumns: 3
+ property int gridRows: 2
+ property bool useGridLayout: false
+
+ property string holdAction: ""
+ property int holdActionIndex: -1
+ property real holdProgress: 0
+ property bool showHoldHint: false
+
+ property var powerActionConfirmOverride: undefined
+ property var powerActionHoldDurationOverride: undefined
+ property var powerMenuActionsOverride: undefined
+ property var powerMenuDefaultActionOverride: undefined
+ property var powerMenuGridLayoutOverride: undefined
+ property var requiredActions: []
+
+ readonly property bool needsConfirmation: powerActionConfirmOverride !== undefined ? powerActionConfirmOverride : SettingsData.powerActionConfirm
+ readonly property int holdDurationMs: (powerActionHoldDurationOverride !== undefined ? powerActionHoldDurationOverride : SettingsData.powerActionHoldDuration) * 1000
+
+ signal closed
+
+ function updateVisibleActions() {
+ const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
+ const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
+ let filtered = allActions.filter(action => {
+ if (action === "hibernate" && !hibernateSupported)
+ return false;
+ if (action === "lock")
+ return false;
+ if (action === "restart")
+ return false;
+ if (action === "logout" && !showLogout)
+ return false;
+ return true;
+ });
+
+ for (const action of requiredActions) {
+ if (!filtered.includes(action))
+ filtered.push(action);
+ }
+
+ visibleActions = filtered;
+
+ useGridLayout = powerMenuGridLayoutOverride !== undefined ? powerMenuGridLayoutOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false);
+ if (!useGridLayout)
+ return;
+ const count = visibleActions.length;
+ if (count === 0) {
+ gridColumns = 1;
+ gridRows = 1;
+ return;
+ }
+
+ if (count <= 3) {
+ gridColumns = 1;
+ gridRows = count;
+ return;
+ }
+
+ if (count === 4) {
+ gridColumns = 2;
+ gridRows = 2;
+ return;
+ }
+
+ gridColumns = 3;
+ gridRows = Math.ceil(count / 3);
+ }
+
+ function getDefaultActionIndex() {
+ const defaultAction = powerMenuDefaultActionOverride !== undefined ? powerMenuDefaultActionOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend");
+ const index = visibleActions.indexOf(defaultAction);
+ return index >= 0 ? index : 0;
+ }
+
+ function getActionAtIndex(index) {
+ if (index < 0 || index >= visibleActions.length)
+ return "";
+ return visibleActions[index];
+ }
+
+ function getActionData(action) {
+ switch (action) {
+ case "reboot":
+ return {
+ "icon": "restart_alt",
+ "label": I18n.tr("Reboot"),
+ "key": "R"
+ };
+ case "logout":
+ return {
+ "icon": "logout",
+ "label": I18n.tr("Log Out"),
+ "key": "X"
+ };
+ case "poweroff":
+ return {
+ "icon": "power_settings_new",
+ "label": I18n.tr("Power Off"),
+ "key": "P"
+ };
+ case "suspend":
+ return {
+ "icon": "bedtime",
+ "label": I18n.tr("Suspend"),
+ "key": "S"
+ };
+ case "hibernate":
+ return {
+ "icon": "ac_unit",
+ "label": I18n.tr("Hibernate"),
+ "key": "H"
+ };
+ default:
+ return {
+ "icon": "help",
+ "label": action,
+ "key": "?"
+ };
+ }
+ }
+
+ function actionNeedsConfirm(action) {
+ return action !== "lock" && action !== "restart";
+ }
+
+ function startHold(action, actionIndex) {
+ if (!needsConfirmation || !actionNeedsConfirm(action)) {
+ executeAction(action);
+ return;
+ }
+ holdAction = action;
+ holdActionIndex = actionIndex;
+ holdProgress = 0;
+ showHoldHint = false;
+ holdTimer.start();
+ }
+
+ function cancelHold() {
+ if (holdAction === "")
+ return;
+ const wasHolding = holdProgress > 0;
+ holdTimer.stop();
+ if (wasHolding && holdProgress < 1) {
+ showHoldHint = true;
+ hintTimer.restart();
+ }
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ }
+
+ function completeHold() {
+ if (holdProgress < 1) {
+ cancelHold();
+ return;
+ }
+ const action = holdAction;
+ holdTimer.stop();
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ executeAction(action);
+ }
+
+ function executeAction(action) {
+ if (!action)
+ return;
+ if (typeof SessionService === "undefined")
+ return;
+ hide();
+ switch (action) {
+ case "logout":
+ SessionService.logout();
+ break;
+ case "suspend":
+ SessionService.suspend();
+ break;
+ case "hibernate":
+ SessionService.hibernate();
+ break;
+ case "reboot":
+ SessionService.reboot();
+ break;
+ case "poweroff":
+ SessionService.poweroff();
+ break;
+ }
+ }
+
+ function selectOption(action, actionIndex) {
+ startHold(action, actionIndex !== undefined ? actionIndex : -1);
+ }
+
+ function show() {
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ showHoldHint = false;
+ updateVisibleActions();
+ const defaultIndex = getDefaultActionIndex();
+ if (useGridLayout) {
+ selectedRow = Math.floor(defaultIndex / gridColumns);
+ selectedCol = defaultIndex % gridColumns;
+ selectedIndex = defaultIndex;
+ } else {
+ selectedIndex = defaultIndex;
+ }
+ isVisible = true;
+ Qt.callLater(() => powerMenuFocusScope.forceActiveFocus());
+ }
+
+ function hide() {
+ cancelHold();
+ isVisible = false;
+ closed();
+ }
+
+ function handleListNavigation(event, isPressed) {
+ if (!isPressed) {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
+ cancelHold();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ switch (event.key) {
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ break;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ startHold(getActionAtIndex(selectedIndex), selectedIndex);
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (!(event.modifiers & Qt.ControlModifier)) {
+ const idx = visibleActions.indexOf("poweroff");
+ startHold("poweroff", idx);
+ event.accepted = true;
+ } else {
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_R:
+ startHold("reboot", visibleActions.indexOf("reboot"));
+ event.accepted = true;
+ break;
+ case Qt.Key_X:
+ startHold("logout", visibleActions.indexOf("logout"));
+ event.accepted = true;
+ break;
+ case Qt.Key_S:
+ startHold("suspend", visibleActions.indexOf("suspend"));
+ event.accepted = true;
+ break;
+ case Qt.Key_H:
+ startHold("hibernate", visibleActions.indexOf("hibernate"));
+ event.accepted = true;
+ break;
+ }
+ }
+
+ function handleGridNavigation(event, isPressed) {
+ if (!isPressed) {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_S || event.key === Qt.Key_H || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
+ cancelHold();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ switch (event.key) {
+ case Qt.Key_Left:
+ selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Right:
+ selectedCol = (selectedCol + 1) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ selectedRow = (selectedRow - 1 + gridRows) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ selectedRow = (selectedRow + 1) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ startHold(getActionAtIndex(selectedIndex), selectedIndex);
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedCol = (selectedCol + 1) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (!(event.modifiers & Qt.ControlModifier)) {
+ const idx = visibleActions.indexOf("poweroff");
+ startHold("poweroff", idx);
+ event.accepted = true;
+ } else {
+ selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedRow = (selectedRow + 1) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedRow = (selectedRow - 1 + gridRows) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_R:
+ startHold("reboot", visibleActions.indexOf("reboot"));
+ event.accepted = true;
+ break;
+ case Qt.Key_X:
+ startHold("logout", visibleActions.indexOf("logout"));
+ event.accepted = true;
+ break;
+ case Qt.Key_S:
+ startHold("suspend", visibleActions.indexOf("suspend"));
+ event.accepted = true;
+ break;
+ case Qt.Key_H:
+ startHold("hibernate", visibleActions.indexOf("hibernate"));
+ event.accepted = true;
+ break;
+ }
+ }
+
+ anchors.fill: parent
+ color: Qt.rgba(0, 0, 0, 0.5)
+ visible: isVisible
+ z: 1000
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.hide()
+ }
+
+ Timer {
+ id: holdTimer
+ interval: 16
+ repeat: true
+ onTriggered: {
+ root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs));
+ if (root.holdProgress >= 1) {
+ stop();
+ root.completeHold();
+ }
+ }
+ }
+
+ Timer {
+ id: hintTimer
+ interval: 2000
+ onTriggered: root.showHoldHint = false
+ }
+
+ FocusScope {
+ id: powerMenuFocusScope
+ anchors.fill: parent
+ focus: root.isVisible
+
+ onVisibleChanged: {
+ if (visible)
+ Qt.callLater(() => forceActiveFocus());
+ }
+
+ Keys.onEscapePressed: root.hide()
+ Keys.onPressed: event => {
+ if (event.isAutoRepeat) {
+ event.accepted = true;
+ return;
+ }
+ if (useGridLayout) {
+ handleGridNavigation(event, true);
+ } else {
+ handleListNavigation(event, true);
+ }
+ }
+ Keys.onReleased: event => {
+ if (event.isAutoRepeat) {
+ event.accepted = true;
+ return;
+ }
+ if (useGridLayout) {
+ handleGridNavigation(event, false);
+ } else {
+ handleListNavigation(event, false);
+ }
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: useGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 320
+ height: contentItem.implicitHeight + Theme.spacingL * 2
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainer
+ border.color: Theme.outlineMedium
+ border.width: 1
+
+ Item {
+ id: contentItem
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ implicitHeight: headerRow.height + Theme.spacingM + (useGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
+
+ Row {
+ id: headerRow
+ width: parent.width
+ height: 30
+
+ StyledText {
+ text: I18n.tr("Power Options")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ width: parent.width - 150
+ height: 1
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.hide()
+ }
+ }
+
+ Grid {
+ id: buttonGrid
+ visible: useGridLayout
+ anchors.top: headerRow.bottom
+ anchors.topMargin: Theme.spacingM
+ anchors.horizontalCenter: parent.horizontalCenter
+ columns: root.gridColumns
+ columnSpacing: Theme.spacingS
+ rowSpacing: Theme.spacingS
+ width: parent.width
+
+ Repeater {
+ model: root.visibleActions
+
+ Rectangle {
+ id: gridButtonRect
+ required property int index
+ required property string modelData
+
+ readonly property var actionData: root.getActionData(modelData)
+ readonly property bool isSelected: root.selectedIndex === index
+ readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
+ readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
+
+ width: (contentItem.width - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
+ height: 100
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ if (mouseArea.containsMouse)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
+ }
+ border.color: isSelected ? Theme.primary : "transparent"
+ border.width: isSelected ? 2 : 0
+
+ Rectangle {
+ id: gridProgressMask
+ anchors.fill: parent
+ radius: parent.radius
+ visible: false
+ layer.enabled: true
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: gridButtonRect.isHolding
+ layer.enabled: gridButtonRect.isHolding
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskSource: gridProgressMask
+ maskSpreadAtMin: 1
+ maskThresholdMin: 0.5
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * root.holdProgress
+ color: {
+ if (gridButtonRect.modelData === "poweroff")
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
+ if (gridButtonRect.modelData === "reboot")
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ }
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: gridButtonRect.actionData.icon
+ size: Theme.iconSize + 8
+ color: {
+ if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
+ return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: gridButtonRect.actionData.label
+ font.pixelSize: Theme.fontSizeMedium
+ color: {
+ if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
+ return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Rectangle {
+ width: 20
+ height: 16
+ radius: 4
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: gridButtonRect.actionData.key
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: {
+ root.selectedRow = Math.floor(index / root.gridColumns);
+ root.selectedCol = index % root.gridColumns;
+ root.selectedIndex = index;
+ root.startHold(modelData, index);
+ }
+ onReleased: root.cancelHold()
+ onCanceled: root.cancelHold()
+ }
+ }
+ }
+ }
+
+ Column {
+ id: buttonColumn
+ visible: !useGridLayout
+ anchors.top: headerRow.bottom
+ anchors.topMargin: Theme.spacingM
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.visibleActions
+
+ Rectangle {
+ id: listButtonRect
+ required property int index
+ required property string modelData
+
+ readonly property var actionData: root.getActionData(modelData)
+ readonly property bool isSelected: root.selectedIndex === index
+ readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
+ readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
+
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ if (listMouseArea.containsMouse)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
+ }
+ border.color: isSelected ? Theme.primary : "transparent"
+ border.width: isSelected ? 2 : 0
+
+ Rectangle {
+ id: listProgressMask
+ anchors.fill: parent
+ radius: parent.radius
+ visible: false
+ layer.enabled: true
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: listButtonRect.isHolding
+ layer.enabled: listButtonRect.isHolding
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskSource: listProgressMask
+ maskSpreadAtMin: 1
+ maskThresholdMin: 0.5
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * root.holdProgress
+ color: {
+ if (listButtonRect.modelData === "poweroff")
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
+ if (listButtonRect.modelData === "reboot")
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ }
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: listButtonRect.actionData.icon
+ size: Theme.iconSize + 4
+ color: {
+ if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
+ return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: listButtonRect.actionData.label
+ font.pixelSize: Theme.fontSizeMedium
+ color: {
+ if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
+ return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: 28
+ height: 20
+ radius: 4
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ text: listButtonRect.actionData.key
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+ }
+
+ MouseArea {
+ id: listMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: {
+ root.selectedIndex = index;
+ root.startHold(modelData, index);
+ }
+ onReleased: root.cancelHold()
+ onCanceled: root.cancelHold()
+ }
+ }
+ }
+ }
+
+ Row {
+ id: hintRow
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: Theme.spacingS
+ spacing: Theme.spacingXS
+ visible: root.needsConfirmation
+ opacity: root.showHoldHint ? 1 : 0.5
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 150
+ }
+ }
+
+ DankIcon {
+ name: root.showHoldHint ? "warning" : "touch_app"
+ size: Theme.fontSizeSmall
+ color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ readonly property real totalMs: root.holdDurationMs
+ readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
+ readonly property real durationSec: root.holdDurationMs / 1000
+ text: {
+ if (root.showHoldHint)
+ return I18n.tr("Hold longer to confirm");
+ if (root.holdProgress > 0) {
+ if (totalMs < 1000)
+ return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
+ return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
+ }
+ if (totalMs < 1000)
+ return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
+ return I18n.tr("Hold to confirm (%1s)").arg(durationSec);
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: updateVisibleActions()
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml
new file mode 100644
index 0000000..b296e98
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml
@@ -0,0 +1,1707 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import QtQuick.Window
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Io
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ function encodeFileUrl(path) {
+ if (!path)
+ return "";
+ return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
+ }
+
+ property string passwordBuffer: ""
+ property bool demoMode: false
+ property string screenName: ""
+ property bool unlocking: false
+ property string pamState: ""
+ property string hyprlandCurrentLayout: ""
+ property string hyprlandKeyboard: ""
+ property int hyprlandLayoutCount: 0
+ property bool lockerReadySent: false
+ property bool lockerReadyArmed: false
+
+ signal unlockRequested
+
+ function resetLockState() {
+ lockerReadySent = false;
+ lockerReadyArmed = true;
+ unlocking = false;
+ pamState = "";
+ if (pam)
+ pam.lockMessage = "";
+ }
+
+ function currentAuthFeedbackText() {
+ if (!pam)
+ return "";
+ if (pam.u2fState === "insert" && !pam.u2fPending)
+ return I18n.tr("Insert your security key...");
+ if (pam.u2fState === "waiting" && !pam.u2fPending)
+ return I18n.tr("Touch your security key...");
+ if (pam.lockMessage && pam.lockMessage.length > 0)
+ return pam.lockMessage;
+ if (root.pamState === "error")
+ return I18n.tr("Authentication error - try again");
+ if (root.pamState === "max")
+ return I18n.tr("Too many attempts - locked out");
+ if (root.pamState === "fail")
+ return I18n.tr("Incorrect password - try again");
+ if (pam.fprintState === "error") {
+ const detail = (pam.fprint.message || "").trim();
+ return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
+ }
+ if (pam.fprintState === "max")
+ return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
+ if (pam.fprintState === "fail")
+ return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
+ return "";
+ }
+
+ function authFeedbackIsHint() {
+ return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
+ }
+
+ Component.onCompleted: {
+ WeatherService.addRef();
+ UserInfoService.getUserInfo();
+
+ if (CompositorService.isHyprland)
+ updateHyprlandLayout();
+
+ lockerReadyArmed = true;
+ }
+
+ Component.onDestruction: {
+ WeatherService.removeRef();
+ }
+
+ function sendLockerReadyOnce() {
+ if (lockerReadySent)
+ return;
+ if (root.unlocking)
+ return;
+ lockerReadySent = true;
+ if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
+ DMSService.sendRequest("loginctl.lockerReady", null, resp => {
+ if (resp?.error)
+ console.warn("lockerReady failed:", resp.error);
+ else
+ console.log("lockerReady sent (afterAnimating/afterRendering)");
+ });
+ }
+ }
+
+ function maybeSend() {
+ if (!lockerReadyArmed)
+ return;
+ if (root.unlocking)
+ return;
+ if (!root.visible || root.opacity <= 0)
+ return;
+ Qt.callLater(() => {
+ if (root.visible && root.opacity > 0 && !root.unlocking)
+ sendLockerReadyOnce();
+ });
+ }
+
+ Connections {
+ target: root.Window.window
+ enabled: target !== null
+
+ function onAfterAnimating() {
+ maybeSend();
+ }
+ function onAfterRendering() {
+ maybeSend();
+ }
+ }
+
+ onVisibleChanged: maybeSend()
+ onOpacityChanged: maybeSend()
+
+ function updateHyprlandLayout() {
+ if (CompositorService.isHyprland) {
+ hyprlandLayoutProcess.running = true;
+ }
+ }
+
+ Process {
+ id: hyprlandLayoutProcess
+ running: false
+ command: ["hyprctl", "-j", "devices"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const data = JSON.parse(text);
+ const mainKeyboard = data.keyboards.find(kb => kb.main === true);
+ if (!mainKeyboard) {
+ hyprlandCurrentLayout = "";
+ hyprlandLayoutCount = 0;
+ return;
+ }
+ hyprlandKeyboard = mainKeyboard.name;
+ if (mainKeyboard.active_keymap) {
+ const parts = mainKeyboard.active_keymap.split(" ");
+ hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase();
+ } else {
+ hyprlandCurrentLayout = "";
+ }
+ hyprlandLayoutCount = mainKeyboard.layout ? mainKeyboard.layout.split(",").length : 0;
+ } catch (e) {
+ hyprlandCurrentLayout = "";
+ hyprlandLayoutCount = 0;
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: CompositorService.isHyprland ? Hyprland : null
+ enabled: CompositorService.isHyprland
+
+ function onRawEvent(event) {
+ if (event.name === "activelayout")
+ updateHyprlandLayout();
+ }
+ }
+
+ Loader {
+ anchors.fill: parent
+ active: {
+ var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
+ return !currentWallpaper || (currentWallpaper && currentWallpaper.startsWith("#"));
+ }
+ asynchronous: true
+
+ sourceComponent: DankBackdrop {
+ screenName: root.screenName
+ }
+ }
+
+ Image {
+ id: wallpaperBackground
+
+ anchors.fill: parent
+ source: {
+ var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
+ return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
+ }
+ fillMode: Theme.getFillMode(SessionData.getMonitorWallpaperFillMode(screenName))
+ smooth: true
+ asynchronous: false
+ cache: true
+ visible: source !== ""
+ layer.enabled: true
+
+ layer.effect: MultiEffect {
+ autoPaddingEnabled: false
+ blurEnabled: true
+ blur: 0.8
+ blurMax: 32
+ blurMultiplier: 1
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "black"
+ opacity: 0.4
+ }
+
+ SystemClock {
+ id: systemClock
+
+ precision: SystemClock.Seconds
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+
+ Item {
+ id: clockContainer
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.verticalCenter
+ anchors.bottomMargin: 60
+ width: parent.width
+ height: clockText.implicitHeight
+ visible: SettingsData.lockScreenShowTime
+
+ Row {
+ id: clockText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ spacing: 0
+
+ property string fullTimeStr: {
+ const format = SettingsData.getEffectiveTimeFormat();
+ return systemClock.date.toLocaleTimeString(Qt.locale(), format);
+ }
+ property var timeParts: fullTimeStr.split(':')
+ property string hours: timeParts[0] || ""
+ property string minutes: timeParts[1] || ""
+ property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
+ property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
+ property string ampm: {
+ const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
+ return match ? match[0].trim() : "";
+ }
+ property bool hasSeconds: timeParts.length > 2
+
+ StyledText {
+ width: 75
+ text: clockText.hours.length > 1 ? clockText.hours[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: ":"
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: clockText.hasSeconds ? ":" : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 75
+ text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ horizontalAlignment: Text.AlignHCenter
+ visible: clockText.hasSeconds
+ }
+
+ StyledText {
+ width: 20
+ text: " "
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.ampm !== ""
+ }
+
+ StyledText {
+ text: clockText.ampm
+ font.pixelSize: 120
+ font.weight: Font.Light
+ color: "white"
+ visible: clockText.ampm !== ""
+ }
+ }
+ }
+
+ StyledText {
+ id: dateText
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: clockContainer.bottom
+ anchors.topMargin: 4
+ visible: SettingsData.lockScreenShowDate
+ text: {
+ if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
+ return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
+ }
+ return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
+ }
+ font.pixelSize: Theme.fontSizeXLarge
+ color: "white"
+ opacity: 0.9
+ }
+
+ Item {
+ id: lockNotificationPanel
+
+ readonly property int notificationMode: SettingsData.lockScreenNotificationMode
+ readonly property var notifications: NotificationService.groupedNotifications
+ readonly property int totalCount: {
+ let count = 0;
+ for (const group of notifications) {
+ count += group.count || 0;
+ }
+ return count;
+ }
+ readonly property bool hasNotifications: totalCount > 0
+ readonly property var appNameGroups: {
+ const groups = {};
+ for (const group of notifications) {
+ const appName = (group.appName || "Unknown").toLowerCase();
+ if (!groups[appName]) {
+ groups[appName] = {
+ appName: group.appName || I18n.tr("Unknown"),
+ count: 0,
+ latestNotification: group.latestNotification
+ };
+ }
+ groups[appName].count += group.count || 0;
+ if (group.latestNotification && (!groups[appName].latestNotification || group.latestNotification.time > groups[appName].latestNotification.time)) {
+ groups[appName].latestNotification = group.latestNotification;
+ }
+ }
+ return Object.values(groups).sort((a, b) => b.count - a.count);
+ }
+
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: dateText.visible ? dateText.bottom : clockContainer.bottom
+ anchors.topMargin: Theme.spacingM
+ width: Math.min(380, parent.width - Theme.spacingXL * 2)
+ height: notificationMode === 0 || !hasNotifications ? 0 : contentLoader.height
+ visible: notificationMode > 0 && hasNotifications
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.left: parent.left
+ anchors.right: parent.right
+ active: lockNotificationPanel.notificationMode > 0 && lockNotificationPanel.hasNotifications
+ sourceComponent: {
+ switch (lockNotificationPanel.notificationMode) {
+ case 1:
+ return countOnlyComponent;
+ case 2:
+ return appNamesComponent;
+ case 3:
+ return fullContentComponent;
+ default:
+ return null;
+ }
+ }
+ }
+
+ Component {
+ id: countOnlyComponent
+
+ Rectangle {
+ width: parent.width
+ height: 44
+ radius: Theme.cornerRadius
+ color: Qt.rgba(0, 0, 0, 0.3)
+ border.color: Qt.rgba(1, 1, 1, 0.1)
+ border.width: 1
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "notifications"
+ size: Theme.iconSize
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: lockNotificationPanel.totalCount === 1 ? I18n.tr("1 notification") : I18n.tr("%1 notifications").arg(lockNotificationPanel.totalCount)
+ font.pixelSize: Theme.fontSizeMedium
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+
+ Component {
+ id: appNamesComponent
+
+ Rectangle {
+ width: parent.width
+ height: Math.min(appNamesColumn.implicitHeight + Theme.spacingM * 2, 200)
+ radius: Theme.cornerRadius
+ color: Qt.rgba(0, 0, 0, 0.3)
+ border.color: Qt.rgba(1, 1, 1, 0.1)
+ border.width: 1
+ clip: true
+
+ Flickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ contentHeight: appNamesColumn.implicitHeight
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+
+ Column {
+ id: appNamesColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: lockNotificationPanel.appNameGroups.slice(0, 5)
+
+ Row {
+ required property var modelData
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "notifications"
+ size: Theme.iconSize - 4
+ color: "white"
+ opacity: 0.8
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: modelData.appName || I18n.tr("Unknown")
+ font.pixelSize: Theme.fontSizeMedium
+ color: "white"
+ elide: Text.ElideRight
+ width: parent.width - Theme.iconSize - countBadge.width - Theme.spacingS * 2
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Rectangle {
+ id: countBadge
+ width: countText.implicitWidth + Theme.spacingS * 2
+ height: 20
+ radius: 10
+ color: Qt.rgba(1, 1, 1, 0.2)
+ visible: modelData.count > 1
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: countText
+ anchors.centerIn: parent
+ text: modelData.count > 99 ? "99+" : modelData.count.toString()
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ }
+ }
+ }
+ }
+
+ StyledText {
+ visible: lockNotificationPanel.appNameGroups.length > 5
+ text: I18n.tr("+ %1 more").arg(lockNotificationPanel.appNameGroups.length - 5)
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ opacity: 0.7
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: fullContentComponent
+
+ Rectangle {
+ width: parent.width
+ height: Math.min(fullContentColumn.implicitHeight + Theme.spacingM * 2, 280)
+ radius: Theme.cornerRadius
+ color: Qt.rgba(0, 0, 0, 0.3)
+ border.color: Qt.rgba(1, 1, 1, 0.1)
+ border.width: 1
+ clip: true
+
+ Flickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ contentHeight: fullContentColumn.implicitHeight
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+
+ Column {
+ id: fullContentColumn
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Repeater {
+ model: {
+ const items = [];
+ for (const group of lockNotificationPanel.appNameGroups) {
+ if (group.latestNotification && items.length < 5) {
+ items.push(group.latestNotification);
+ }
+ }
+ return items;
+ }
+
+ Rectangle {
+ required property var modelData
+ required property int index
+ width: parent.width
+ height: notifContent.implicitHeight + Theme.spacingS * 2
+ radius: Theme.cornerRadius - 4
+ color: Qt.rgba(1, 1, 1, 0.05)
+
+ Column {
+ id: notifContent
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingS
+ spacing: 2
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: modelData.appName || I18n.tr("Unknown")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: "white"
+ opacity: 0.7
+ elide: Text.ElideRight
+ width: parent.width - timeText.implicitWidth - Theme.spacingXS
+ }
+
+ StyledText {
+ id: timeText
+ text: modelData.timeStr || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ opacity: 0.5
+ }
+ }
+
+ StyledText {
+ width: parent.width
+ text: modelData.summary || ""
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: "white"
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ visible: text.length > 0
+ }
+
+ StyledText {
+ width: parent.width
+ text: {
+ const body = modelData.body || "";
+ return body.replace(/<[^>]*>/g, '').replace(/\n/g, ' ');
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ opacity: 0.8
+ elide: Text.ElideRight
+ maximumLineCount: 2
+ wrapMode: Text.WordWrap
+ visible: text.length > 0
+ }
+ }
+ }
+ }
+
+ StyledText {
+ visible: lockNotificationPanel.appNameGroups.length > 5
+ text: I18n.tr("+ %1 more").arg(lockNotificationPanel.appNameGroups.length - 5)
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ opacity: 0.7
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: passwordLayout
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: lockNotificationPanel.visible ? lockNotificationPanel.bottom : (dateText.visible ? dateText.bottom : clockContainer.bottom)
+ anchors.topMargin: Theme.spacingL
+ spacing: Theme.spacingM
+ width: 380
+
+ RowLayout {
+ spacing: Theme.spacingL
+ Layout.fillWidth: true
+
+ DankCircularImage {
+ Layout.preferredWidth: 60
+ Layout.preferredHeight: 60
+ imageSource: {
+ if (PortalService.profileImage === "")
+ return "";
+ if (PortalService.profileImage.startsWith("/"))
+ return encodeFileUrl(PortalService.profileImage);
+ return PortalService.profileImage;
+ }
+ fallbackIcon: "person"
+ visible: SettingsData.lockScreenShowProfileImage
+ }
+
+ Rectangle {
+ property bool showPassword: false
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 60
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
+ border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
+ border.width: passwordField.activeFocus ? 2 : 1
+ visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0
+
+ Item {
+ id: lockIconContainer
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 20
+ height: 20
+
+ DankIcon {
+ id: lockIcon
+
+ anchors.centerIn: parent
+ name: {
+ if (pam.u2fPending)
+ return "passkey";
+ if (pam.fprint.tries >= SettingsData.maxFprintTries)
+ return "fingerprint_off";
+ if (pam.fprint.active)
+ return "fingerprint";
+ if (pam.u2f.active)
+ return "passkey";
+ return "lock";
+ }
+ size: 20
+ color: {
+ if (pam.fprint.tries >= SettingsData.maxFprintTries)
+ return Theme.error;
+ if (pam.u2fState !== "")
+ return Theme.tertiary;
+ return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
+ }
+ opacity: pam.passwd.active ? 0 : 1
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ TextInput {
+ id: passwordField
+
+ anchors.fill: parent
+ anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
+ anchors.rightMargin: {
+ let margin = Theme.spacingM;
+ if (loadingSpinner.visible) {
+ margin += loadingSpinner.width;
+ }
+ if (enterButton.visible) {
+ margin += enterButton.width + 2;
+ }
+ if (virtualKeyboardButton.visible) {
+ margin += virtualKeyboardButton.width;
+ }
+ if (revealButton.visible) {
+ margin += revealButton.width;
+ }
+ return margin;
+ }
+ opacity: 0
+ focus: true
+ enabled: !demoMode
+ activeFocusOnTab: !demoMode
+ echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
+ onTextChanged: {
+ if (!demoMode) {
+ root.passwordBuffer = text;
+ }
+ }
+ onAccepted: {
+ if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
+ pam.passwd.start();
+ }
+ }
+ Keys.onPressed: event => {
+ if (demoMode) {
+ return;
+ }
+
+ if (root.unlocking) {
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Escape) {
+ if (pam.u2fPending) {
+ pam.cancelU2fPending();
+ event.accepted = true;
+ return;
+ }
+ clear();
+ }
+
+ if (pam.passwd.active) {
+ console.log("PAM is active, ignoring input");
+ event.accepted = true;
+ return;
+ }
+ }
+
+ Component.onCompleted: {
+ if (!demoMode) {
+ forceActiveFocus();
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible && !demoMode) {
+ forceActiveFocus();
+ }
+ }
+
+ onActiveFocusChanged: {
+ if (!activeFocus && !demoMode && visible && passwordField && !powerMenu.isVisible) {
+ Qt.callLater(() => {
+ if (passwordField && passwordField.forceActiveFocus) {
+ passwordField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ onEnabledChanged: {
+ if (enabled && !demoMode && visible && passwordField && !powerMenu.isVisible) {
+ Qt.callLater(() => {
+ if (passwordField && passwordField.forceActiveFocus) {
+ passwordField.forceActiveFocus();
+ }
+ });
+ }
+ }
+ }
+
+ KeyboardController {
+ id: keyboardController
+ target: passwordField
+ rootObject: root
+ }
+
+ StyledText {
+ id: placeholder
+
+ anchors.left: lockIconContainer.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (demoMode) {
+ return "";
+ }
+ if (root.unlocking) {
+ return "Unlocking...";
+ }
+ if (pam.u2fPending) {
+ if (pam.u2fState === "insert")
+ return "Insert your security key...";
+ return "Touch your security key...";
+ }
+ if (pam.passwd.active) {
+ return "Authenticating...";
+ }
+ return "Password...";
+ }
+ color: root.unlocking ? Theme.primary : (pam.passwd.active ? Theme.primary : Theme.outline)
+ font.pixelSize: Theme.fontSizeMedium
+ opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ StyledText {
+ anchors.left: lockIconContainer.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ text: {
+ if (demoMode) {
+ return "••••••••";
+ }
+ if (parent.showPassword) {
+ return root.passwordBuffer;
+ }
+ return "•".repeat(root.passwordBuffer.length);
+ }
+ color: Theme.surfaceText
+ font.pixelSize: parent.showPassword ? Theme.fontSizeMedium : Theme.fontSizeLarge
+ opacity: (demoMode || root.passwordBuffer.length > 0) ? 1 : 0
+ clip: true
+ elide: Text.ElideNone
+ horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankActionButton {
+ id: revealButton
+
+ anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))
+ anchors.rightMargin: 0
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: parent.showPassword ? "visibility_off" : "visibility"
+ buttonSize: 32
+ visible: !demoMode && root.passwordBuffer.length > 0 && !pam.passwd.active && !root.unlocking
+ enabled: visible
+ onClicked: parent.showPassword = !parent.showPassword
+ }
+ DankActionButton {
+ id: virtualKeyboardButton
+
+ anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)
+ anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "keyboard"
+ buttonSize: 32
+ visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
+ enabled: visible
+ onClicked: {
+ if (keyboardController.isKeyboardActive) {
+ keyboardController.hide();
+ } else {
+ keyboardController.show();
+ }
+ }
+ }
+
+ Rectangle {
+ id: loadingSpinner
+
+ anchors.right: enterButton.visible ? enterButton.left : parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 24
+ height: 24
+ radius: 12
+ color: "transparent"
+ visible: !demoMode && (pam.passwd.active || root.unlocking)
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check_circle"
+ size: 20
+ color: Theme.primary
+ visible: root.unlocking
+
+ SequentialAnimation on scale {
+ running: root.unlocking
+
+ NumberAnimation {
+ from: 0
+ to: 1.2
+ duration: Anims.durShort
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasizedDecel
+ }
+
+ NumberAnimation {
+ from: 1.2
+ to: 1
+ duration: Anims.durShort
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasizedAccel
+ }
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: pam.passwd.active && !root.unlocking
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.centerIn: parent
+ color: "transparent"
+ border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
+ border.width: 2
+ }
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.centerIn: parent
+ color: "transparent"
+ border.color: Theme.primary
+ border.width: 2
+
+ Rectangle {
+ width: parent.width
+ height: parent.height / 2
+ anchors.top: parent.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
+ }
+
+ RotationAnimation on rotation {
+ running: pam.passwd.active && !root.unlocking
+ loops: Animation.Infinite
+ duration: Anims.durLong
+ from: 0
+ to: 360
+ }
+ }
+ }
+ }
+
+ DankActionButton {
+ id: enterButton
+
+ anchors.right: parent.right
+ anchors.rightMargin: 2
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "keyboard_return"
+ buttonSize: 36
+ visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
+ enabled: !demoMode
+ onClicked: {
+ if (!demoMode && !root.unlocking && !pam.u2fPending) {
+ pam.passwd.start();
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: authFeedbackText
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: text.length > 0 ? Math.min(implicitHeight, Math.ceil(Theme.fontSizeSmall * 4.5)) : 0
+ text: root.currentAuthFeedbackText()
+ color: root.authFeedbackIsHint() ? Theme.outline : Theme.error
+ font.pixelSize: Theme.fontSizeSmall
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ maximumLineCount: 3
+ elide: Text.ElideRight
+ opacity: text.length > 0 ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ Row {
+ anchors.top: passwordLayout.bottom
+ anchors.topMargin: Theme.spacingS
+ anchors.horizontalCenter: passwordLayout.horizontalCenter
+ spacing: 4
+ opacity: DMSService.capsLockState ? 1 : 0
+
+ DankIcon {
+ name: "shift_lock"
+ size: 14
+ color: Theme.error
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Caps Lock is on")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ StyledText {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.margins: Theme.spacingXL
+ text: I18n.tr("DEMO MODE - Click anywhere to exit")
+ font.pixelSize: Theme.fontSizeSmall
+ color: "white"
+ opacity: 0.7
+ visible: demoMode
+ }
+
+ Row {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingXL
+ spacing: Theme.spacingL
+ visible: SettingsData.lockScreenShowSystemIcons
+
+ Item {
+ width: keyboardLayoutRow.width
+ height: keyboardLayoutRow.height
+ anchors.verticalCenter: parent.verticalCenter
+ visible: {
+ if (CompositorService.isNiri) {
+ return NiriService.keyboardLayoutNames.length > 1;
+ } else if (CompositorService.isHyprland) {
+ return hyprlandLayoutCount > 1;
+ }
+ return false;
+ }
+
+ Row {
+ id: keyboardLayoutRow
+ spacing: 4
+
+ Item {
+ width: Theme.iconSize
+ height: Theme.iconSize
+
+ DankIcon {
+ name: "keyboard"
+ size: Theme.iconSize
+ color: "white"
+ anchors.centerIn: parent
+ }
+ }
+
+ Item {
+ width: childrenRect.width
+ height: Theme.iconSize
+
+ StyledText {
+ text: {
+ if (CompositorService.isNiri) {
+ const layout = NiriService.getCurrentKeyboardLayoutName();
+ if (!layout)
+ return "";
+ const parts = layout.split(" ");
+ if (parts.length > 0) {
+ return parts[0].substring(0, 2).toUpperCase();
+ }
+ return layout.substring(0, 2).toUpperCase();
+ } else if (CompositorService.isHyprland) {
+ return hyprlandCurrentLayout;
+ }
+ return "";
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ MouseArea {
+ id: keyboardLayoutArea
+ anchors.fill: parent
+ enabled: !demoMode
+ hoverEnabled: enabled
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ if (CompositorService.isNiri) {
+ NiriService.cycleKeyboardLayout();
+ } else if (CompositorService.isHyprland) {
+ Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]);
+ updateHyprlandLayout();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer
+ }
+
+ Row {
+ spacing: Theme.spacingS
+ visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer
+ anchors.verticalCenter: parent.verticalCenter
+
+ Item {
+ width: 20
+ height: Theme.iconSize
+ anchors.verticalCenter: parent.verticalCenter
+
+ Loader {
+ active: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing
+
+ sourceComponent: Component {
+ Ref {
+ service: CavaService
+ }
+ }
+ }
+
+ Timer {
+ running: !CavaService.cavaAvailable && MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing
+ interval: 256
+ repeat: true
+ onTriggered: {
+ CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 1.5
+
+ Repeater {
+ model: 6
+ delegate: Rectangle {
+ required property int index
+
+ width: 2
+ height: {
+ if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) {
+ const rawLevel = CavaService.values[index] || 0;
+ const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
+ const maxHeight = Theme.iconSize - 2;
+ const minHeight = 3;
+ return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
+ }
+ return 3;
+ }
+ radius: 1.5
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Anims.durShort
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.standardDecel
+ }
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ text: {
+ const player = MprisController.activePlayer;
+ if (!player?.trackTitle)
+ return "";
+ const title = player.trackTitle;
+ const artist = player.trackArtist || "";
+ return artist ? title + " • " + artist : title;
+ }
+ font.pixelSize: Theme.fontSizeLarge
+ color: "white"
+ opacity: 0.9
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ width: Math.min(implicitWidth, 400)
+ wrapMode: Text.NoWrap
+ maximumLineCount: 1
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.verticalCenter: parent.verticalCenter
+ color: prevArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent"
+ visible: MprisController.activePlayer
+ opacity: (MprisController.activePlayer?.canGoPrevious ?? false) ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_previous"
+ size: 12
+ color: "white"
+ }
+
+ MouseArea {
+ id: prevArea
+ anchors.fill: parent
+ enabled: MprisController.activePlayer?.canGoPrevious ?? false
+ hoverEnabled: enabled
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: MprisController.previousOrRewind()
+ }
+ }
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: 12
+ anchors.verticalCenter: parent.verticalCenter
+ color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? Qt.rgba(255, 255, 255, 0.9) : Qt.rgba(255, 255, 255, 0.2)
+ visible: MprisController.activePlayer
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
+ size: 14
+ color: MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing ? "black" : "white"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: MprisController.activePlayer
+ hoverEnabled: enabled
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: MprisController.activePlayer?.togglePlaying()
+ }
+ }
+
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors.verticalCenter: parent.verticalCenter
+ color: nextArea.containsMouse ? Qt.rgba(255, 255, 255, 0.2) : "transparent"
+ visible: MprisController.activePlayer
+ opacity: (MprisController.activePlayer?.canGoNext ?? false) ? 1 : 0.3
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "skip_next"
+ size: 12
+ color: "white"
+ }
+
+ MouseArea {
+ id: nextArea
+ anchors.fill: parent
+ enabled: MprisController.activePlayer?.canGoNext ?? false
+ hoverEnabled: enabled
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: MprisController.activePlayer?.next()
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer && WeatherService.weather.available
+ }
+
+ Row {
+ spacing: 6
+ visible: WeatherService.weather.available
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.iconSize
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
+ }
+
+ Row {
+ spacing: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NetworkService.networkAvailable || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
+
+ DankIcon {
+ name: "screen_record"
+ size: Theme.iconSize - 2
+ color: NiriService.hasActiveCast ? "white" : Qt.rgba(255, 255, 255, 0.5)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NiriService.hasCasts
+ }
+
+ DankIcon {
+ name: {
+ if (NetworkService.wifiToggling)
+ return "sync";
+ switch (NetworkService.networkStatus) {
+ case "ethernet":
+ return "lan";
+ case "vpn":
+ return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon;
+ default:
+ return NetworkService.wifiSignalIcon;
+ }
+ }
+ size: Theme.iconSize - 2
+ color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NetworkService.networkAvailable
+ }
+
+ DankIcon {
+ name: "vpn_lock"
+ size: Theme.iconSize - 2
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
+ }
+
+ DankIcon {
+ name: "bluetooth"
+ size: Theme.iconSize - 2
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BluetoothService.available && BluetoothService.enabled
+ }
+
+ DankIcon {
+ name: {
+ if (!AudioService.sink?.audio) {
+ return "volume_up";
+ }
+ if (AudioService.sink.audio.muted)
+ return "volume_off";
+ if (AudioService.sink.audio.volume === 0)
+ return "volume_mute";
+ if (AudioService.sink.audio.volume * 100 < 33) {
+ return "volume_down";
+ }
+ return "volume_up";
+ }
+ size: Theme.iconSize - 2
+ color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white"
+ anchors.verticalCenter: parent.verticalCenter
+ visible: AudioService.sink && AudioService.sink.audio
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 24
+ color: Qt.rgba(255, 255, 255, 0.2)
+ anchors.verticalCenter: parent.verticalCenter
+ visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio))
+ }
+
+ Row {
+ spacing: 4
+ visible: BatteryService.batteryAvailable
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ name: {
+ if (BatteryService.isCharging) {
+ if (BatteryService.batteryLevel >= 90) {
+ return "battery_charging_full";
+ }
+
+ if (BatteryService.batteryLevel >= 80) {
+ return "battery_charging_90";
+ }
+
+ if (BatteryService.batteryLevel >= 60) {
+ return "battery_charging_80";
+ }
+
+ if (BatteryService.batteryLevel >= 50) {
+ return "battery_charging_60";
+ }
+
+ if (BatteryService.batteryLevel >= 30) {
+ return "battery_charging_50";
+ }
+
+ if (BatteryService.batteryLevel >= 20) {
+ return "battery_charging_30";
+ }
+
+ return "battery_charging_20";
+ }
+ if (BatteryService.isPluggedIn) {
+ if (BatteryService.batteryLevel >= 90) {
+ return "battery_charging_full";
+ }
+
+ if (BatteryService.batteryLevel >= 80) {
+ return "battery_charging_90";
+ }
+
+ if (BatteryService.batteryLevel >= 60) {
+ return "battery_charging_80";
+ }
+
+ if (BatteryService.batteryLevel >= 50) {
+ return "battery_charging_60";
+ }
+
+ if (BatteryService.batteryLevel >= 30) {
+ return "battery_charging_50";
+ }
+
+ if (BatteryService.batteryLevel >= 20) {
+ return "battery_charging_30";
+ }
+
+ return "battery_charging_20";
+ }
+ if (BatteryService.batteryLevel >= 95) {
+ return "battery_full";
+ }
+
+ if (BatteryService.batteryLevel >= 85) {
+ return "battery_6_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 70) {
+ return "battery_5_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 55) {
+ return "battery_4_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 40) {
+ return "battery_3_bar";
+ }
+
+ if (BatteryService.batteryLevel >= 25) {
+ return "battery_2_bar";
+ }
+
+ return "battery_1_bar";
+ }
+ size: Theme.iconSize
+ color: {
+ if (BatteryService.isLowBattery && !BatteryService.isCharging) {
+ return Theme.error;
+ }
+
+ if (BatteryService.isCharging || BatteryService.isPluggedIn) {
+ return Theme.primary;
+ }
+
+ return "white";
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: BatteryService.batteryLevel + "%"
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Light
+ color: "white"
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ DankActionButton {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: Theme.spacingXL
+ visible: SettingsData.lockScreenShowPowerActions
+ iconName: "power_settings_new"
+ iconColor: Theme.error
+ buttonSize: 40
+ onClicked: {
+ if (demoMode) {
+ console.log("Demo: Power Menu");
+ } else {
+ powerMenu.show();
+ }
+ }
+ }
+ }
+
+ Pam {
+ id: pam
+ lockSecured: !demoMode
+ onUnlockRequested: {
+ root.unlocking = true;
+ lockerReadyArmed = false;
+ passwordField.text = "";
+ root.passwordBuffer = "";
+ root.unlockRequested();
+ }
+ onStateChanged: {
+ root.pamState = state;
+ if (state !== "") {
+ root.unlocking = false;
+ placeholderDelay.restart();
+ passwordField.text = "";
+ root.passwordBuffer = "";
+ }
+ }
+ onU2fPendingChanged: {
+ if (u2fPending) {
+ passwordField.text = "";
+ root.passwordBuffer = "";
+ if (keyboardController.isKeyboardActive)
+ keyboardController.hide();
+ }
+ }
+ }
+
+ Connections {
+ target: pam
+
+ function onUnlockInProgressChanged() {
+ if (!pam.unlockInProgress && root.unlocking)
+ root.unlocking = false;
+ }
+ }
+
+ Binding {
+ target: pam
+ property: "buffer"
+ value: root.passwordBuffer
+ }
+
+ Timer {
+ id: placeholderDelay
+
+ interval: 4000
+ onTriggered: root.pamState = ""
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: demoMode
+ onClicked: root.unlockRequested()
+ }
+
+ LockPowerMenu {
+ id: powerMenu
+ showLogout: true
+ onClosed: {
+ if (!demoMode && passwordField && passwordField.forceActiveFocus) {
+ Qt.callLater(() => passwordField.forceActiveFocus());
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml
new file mode 100644
index 0000000..e13eac9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml
@@ -0,0 +1,46 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+
+PanelWindow {
+ id: root
+
+ property bool demoActive: false
+
+ visible: demoActive
+
+ anchors {
+ top: true
+ bottom: true
+ left: true
+ right: true
+ }
+
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+
+ color: "transparent"
+
+ function showDemo(): void {
+ console.log("Showing lock screen demo");
+ demoActive = true;
+ }
+
+ function hideDemo(): void {
+ console.log("Hiding lock screen demo");
+ demoActive = false;
+ }
+
+ Loader {
+ anchors.fill: parent
+ active: demoActive
+ sourceComponent: LockScreenContent {
+ demoMode: true
+ screenName: root.screen?.name ?? ""
+ onUnlockRequested: root.hideDemo()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml
new file mode 100644
index 0000000..9d1beae
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml
@@ -0,0 +1,73 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Wayland
+import qs.Common
+
+FocusScope {
+ id: root
+
+ required property WlSessionLock lock
+ required property string sharedPasswordBuffer
+ required property string screenName
+ required property bool isLocked
+
+ signal passwordChanged(string newPassword)
+ signal unlockRequested
+
+ Keys.onPressed: event => {
+ if (videoScreensaver.active && videoScreensaver.inputEnabled) {
+ videoScreensaver.dismiss();
+ event.accepted = true;
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ }
+
+ LockScreenContent {
+ id: lockContent
+
+ anchors.fill: parent
+ demoMode: false
+ passwordBuffer: root.sharedPasswordBuffer
+ screenName: root.screenName
+ enabled: !videoScreensaver.active
+ focus: !videoScreensaver.active
+ opacity: videoScreensaver.active ? 0 : 1
+ onUnlockRequested: root.unlockRequested()
+ onPasswordBufferChanged: {
+ if (root.sharedPasswordBuffer !== passwordBuffer) {
+ root.passwordChanged(passwordBuffer);
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 200
+ }
+ }
+ }
+
+ VideoScreensaver {
+ id: videoScreensaver
+ anchors.fill: parent
+ screenName: root.screenName
+ }
+
+ Component.onCompleted: forceActiveFocus()
+
+ onIsLockedChanged: {
+ if (isLocked) {
+ forceActiveFocus();
+ lockContent.resetLockState();
+ if (SettingsData.lockScreenVideoEnabled) {
+ videoScreensaver.start();
+ }
+ return;
+ }
+ lockContent.unlocking = false;
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml
new file mode 100644
index 0000000..f5e7859
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml
@@ -0,0 +1,405 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Pam
+import qs.Common
+
+Scope {
+ id: root
+
+ property bool lockSecured: false
+ property bool unlockInProgress: false
+
+ readonly property alias passwd: passwd
+ readonly property alias fprint: fprint
+ readonly property alias u2f: u2f
+ property string lockMessage
+ property string state
+ property string fprintState
+ property string u2fState
+ property bool u2fPending: false
+ property string buffer
+
+ signal flashMsg
+ signal unlockRequested
+
+ function resetAuthFlows(): void {
+ passwd.abort();
+ fprint.abort();
+ u2f.abort();
+ errorRetry.running = false;
+ u2fErrorRetry.running = false;
+ u2fPendingTimeout.running = false;
+ passwdActiveTimeout.running = false;
+ unlockRequestTimeout.running = false;
+ root.u2fPending = false;
+ root.u2fState = "";
+ root.unlockInProgress = false;
+ }
+
+ function recoverFromAuthStall(newState: string): void {
+ resetAuthFlows();
+ root.state = newState;
+ flashMsg();
+ stateReset.restart();
+ fprint.checkAvail();
+ u2f.checkAvail();
+ }
+
+ function completeUnlock(): void {
+ if (!root.unlockInProgress) {
+ root.unlockInProgress = true;
+ passwd.abort();
+ fprint.abort();
+ u2f.abort();
+ errorRetry.running = false;
+ u2fErrorRetry.running = false;
+ u2fPendingTimeout.running = false;
+ root.u2fPending = false;
+ root.u2fState = "";
+ unlockRequestTimeout.restart();
+ unlockRequested();
+ }
+ }
+
+ function proceedAfterPrimaryAuth(): void {
+ if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
+ u2f.startForSecondFactor();
+ } else {
+ completeUnlock();
+ }
+ }
+
+ function cancelU2fPending(): void {
+ if (!root.u2fPending)
+ return;
+ u2f.abort();
+ u2fErrorRetry.running = false;
+ u2fPendingTimeout.running = false;
+ root.u2fPending = false;
+ root.u2fState = "";
+ fprint.checkAvail();
+ }
+
+ FileView {
+ id: dankshellConfigWatcher
+
+ path: "/etc/pam.d/dankshell"
+ printErrors: false
+ }
+
+ FileView {
+ id: nixosMarker
+
+ path: "/etc/NIXOS"
+ printErrors: false
+ }
+
+ FileView {
+ id: u2fConfigWatcher
+
+ path: "/etc/pam.d/dankshell-u2f"
+ printErrors: false
+ }
+
+ // Detects Nix-installed DMS on non-NixOS systems
+ readonly property bool runningFromNixStore: Quickshell.shellDir.startsWith("/nix/store/")
+
+ PamContext {
+ id: passwd
+
+ config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
+ configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
+
+ onMessageChanged: {
+ if (message.startsWith("The account is locked")) {
+ root.lockMessage = message;
+ } else if (root.lockMessage && message.endsWith(" left to unlock)")) {
+ root.lockMessage += "\n" + message;
+ } else if (root.lockMessage && message && message.length > 0) {
+ root.lockMessage = "";
+ }
+ }
+
+ onResponseRequiredChanged: {
+ if (!responseRequired)
+ return;
+
+ respond(root.buffer);
+ }
+
+ onCompleted: res => {
+ if (res === PamResult.Success) {
+ if (!root.unlockInProgress) {
+ fprint.abort();
+ root.proceedAfterPrimaryAuth();
+ }
+ return;
+ }
+
+ unlockRequestTimeout.running = false;
+ root.unlockInProgress = false;
+ root.u2fPending = false;
+ root.u2fState = "";
+ u2fPendingTimeout.running = false;
+ u2f.abort();
+
+ if (res === PamResult.Error)
+ root.state = "error";
+ else if (res === PamResult.MaxTries)
+ root.state = "max";
+ else if (res === PamResult.Failed)
+ root.state = "fail";
+
+ root.flashMsg();
+ stateReset.restart();
+ }
+ }
+
+ Connections {
+ target: passwd
+
+ function onActiveChanged() {
+ if (passwd.active) {
+ passwdActiveTimeout.restart();
+ } else {
+ passwdActiveTimeout.running = false;
+ }
+ }
+ }
+
+ PamContext {
+ id: fprint
+
+ property bool available: SettingsData.lockFingerprintReady
+ property int tries
+ property int errorTries
+
+ function checkAvail(): void {
+ if (!available || !SettingsData.enableFprint || !root.lockSecured) {
+ abort();
+ return;
+ }
+
+ tries = 0;
+ errorTries = 0;
+ start();
+ }
+
+ config: "fprint"
+ configDirectory: Quickshell.shellDir + "/assets/pam"
+
+ onCompleted: res => {
+ if (!available)
+ return;
+
+ if (res === PamResult.Success) {
+ if (!root.unlockInProgress) {
+ passwd.abort();
+ root.proceedAfterPrimaryAuth();
+ }
+ return;
+ }
+
+ if (res === PamResult.Error) {
+ root.fprintState = "error";
+ errorTries++;
+ if (errorTries < 5) {
+ abort();
+ errorRetry.restart();
+ }
+ } else if (res === PamResult.MaxTries) {
+ tries++;
+ if (tries < SettingsData.maxFprintTries) {
+ root.fprintState = "fail";
+ start();
+ } else {
+ root.fprintState = "max";
+ abort();
+ }
+ }
+
+ root.flashMsg();
+ fprintStateReset.start();
+ }
+ }
+
+ PamContext {
+ id: u2f
+
+ property bool available: SettingsData.lockU2fReady
+
+ function checkAvail(): void {
+ if (!available || !SettingsData.enableU2f || !root.lockSecured) {
+ abort();
+ return;
+ }
+
+ if (SettingsData.u2fMode === "or") {
+ start();
+ }
+ }
+
+ function startForSecondFactor(): void {
+ if (!available || !SettingsData.enableU2f) {
+ root.completeUnlock();
+ return;
+ }
+ abort();
+ root.u2fPending = true;
+ root.u2fState = "";
+ u2fPendingTimeout.restart();
+ start();
+ }
+
+ config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
+ configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
+
+ onMessageChanged: {
+ if (message.toLowerCase().includes("touch"))
+ root.u2fState = "waiting";
+ }
+
+ onCompleted: res => {
+ if (!available || root.unlockInProgress)
+ return;
+
+ if (res === PamResult.Success) {
+ root.completeUnlock();
+ return;
+ }
+
+ if (res === PamResult.Error || res === PamResult.MaxTries || res === PamResult.Failed) {
+ abort();
+
+ if (root.u2fPending) {
+ if (root.u2fState === "waiting") {
+ // AND mode: device was found but auth failed → back to password
+ root.u2fPending = false;
+ root.u2fState = "";
+ fprint.checkAvail();
+ } else {
+ // AND mode: no device found → keep pending, show "Insert...", retry
+ root.u2fState = "insert";
+ u2fErrorRetry.restart();
+ }
+ } else {
+ // OR mode: prompt to insert key, silently retry
+ root.u2fState = "insert";
+ u2fErrorRetry.restart();
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: errorRetry
+
+ interval: 800
+ onTriggered: fprint.start()
+ }
+
+ Timer {
+ id: u2fErrorRetry
+
+ interval: 800
+ onTriggered: u2f.start()
+ }
+
+ Timer {
+ id: u2fPendingTimeout
+
+ interval: 30000
+ onTriggered: root.cancelU2fPending()
+ }
+
+ Timer {
+ id: passwdActiveTimeout
+
+ interval: 15000
+ onTriggered: {
+ if (passwd.active)
+ root.recoverFromAuthStall("error");
+ }
+ }
+
+ Timer {
+ id: unlockRequestTimeout
+
+ interval: 8000
+ onTriggered: {
+ if (root.unlockInProgress)
+ root.recoverFromAuthStall("error");
+ }
+ }
+
+ Timer {
+ id: stateReset
+
+ interval: 4000
+ onTriggered: {
+ if (root.state !== "max")
+ root.state = "";
+ }
+ }
+
+ Timer {
+ id: fprintStateReset
+
+ interval: 4000
+ onTriggered: {
+ root.fprintState = "";
+ fprint.errorTries = 0;
+ }
+ }
+
+ onLockSecuredChanged: {
+ if (lockSecured) {
+ SettingsData.refreshAuthAvailability();
+ root.state = "";
+ root.fprintState = "";
+ root.u2fState = "";
+ root.u2fPending = false;
+ root.lockMessage = "";
+ root.resetAuthFlows();
+ fprint.checkAvail();
+ u2f.checkAvail();
+ } else {
+ root.resetAuthFlows();
+ }
+ }
+
+ Connections {
+ target: SettingsData
+
+ function onEnableFprintChanged(): void {
+ fprint.checkAvail();
+ }
+
+ function onLockFingerprintReadyChanged(): void {
+ fprint.checkAvail();
+ }
+
+ function onEnableU2fChanged(): void {
+ u2f.checkAvail();
+ }
+
+ function onLockU2fReadyChanged(): void {
+ u2f.checkAvail();
+ }
+
+ function onU2fModeChanged(): void {
+ if (root.lockSecured) {
+ u2f.abort();
+ u2fErrorRetry.running = false;
+ u2fPendingTimeout.running = false;
+ unlockRequestTimeout.running = false;
+ root.u2fPending = false;
+ root.u2fState = "";
+ u2f.checkAvail();
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml
new file mode 100644
index 0000000..129f507
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml
@@ -0,0 +1,200 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Io
+import qs.Common
+import qs.Services
+
+Item {
+ id: root
+
+ required property string screenName
+ property bool active: false
+ property string videoSource: ""
+ property bool inputEnabled: false
+ property point lastMousePos: Qt.point(-1, -1)
+ property bool mouseInitialized: false
+ property var videoPlayer: null
+
+ signal dismissed
+
+ visible: active
+ z: 1000
+
+ Rectangle {
+ id: background
+ anchors.fill: parent
+ color: "black"
+ visible: root.active
+ }
+
+ Timer {
+ id: inputEnableTimer
+ interval: 500
+ onTriggered: root.inputEnabled = true
+ }
+
+ Process {
+ id: videoPicker
+ property string result: ""
+ property string folder: ""
+
+ command: ["sh", "-c", "find '" + folder + "' -maxdepth 1 -type f \\( " + "-iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' -o " + "-iname '*.mov' -o -iname '*.avi' -o -iname '*.m4v' " + "\\) 2>/dev/null | shuf -n1"]
+
+ stdout: SplitParser {
+ onRead: data => {
+ const path = data.trim();
+ if (path) {
+ videoPicker.result = path;
+ root.videoSource = "file://" + path;
+ }
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode !== 0 || !videoPicker.result) {
+ console.warn("VideoScreensaver: no video found in folder");
+ ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("No video found in folder"));
+ root.dismiss();
+ }
+ }
+ }
+
+ Process {
+ id: fileChecker
+ command: ["test", "-d", SettingsData.lockScreenVideoPath]
+
+ onExited: exitCode => {
+ const isDir = exitCode === 0;
+ const videoPath = SettingsData.lockScreenVideoPath;
+
+ if (isDir) {
+ videoPicker.folder = videoPath;
+ videoPicker.running = true;
+ } else if (SettingsData.lockScreenVideoCycling) {
+ const parentFolder = videoPath.substring(0, videoPath.lastIndexOf('/'));
+ videoPicker.folder = parentFolder;
+ videoPicker.running = true;
+ } else {
+ root.videoSource = "file://" + videoPath;
+ }
+ }
+ }
+
+ function createVideoPlayer() {
+ if (videoPlayer)
+ return true;
+
+ try {
+ videoPlayer = Qt.createQmlObject(`
+ import QtQuick
+ import QtMultimedia
+ Video {
+ anchors.fill: parent
+ fillMode: VideoOutput.PreserveAspectCrop
+ loops: MediaPlayer.Infinite
+ volume: 0
+ }
+ `, background, "VideoScreensaver.VideoPlayer");
+
+ videoPlayer.errorOccurred.connect((error, errorString) => {
+ console.warn("VideoScreensaver: playback error:", errorString);
+ ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
+ root.dismiss();
+ });
+
+ return true;
+ } catch (e) {
+ console.warn("VideoScreensaver: Failed to create video player:", e);
+ return false;
+ }
+ }
+
+ function destroyVideoPlayer() {
+ if (videoPlayer) {
+ videoPlayer.stop();
+ videoPlayer.destroy();
+ videoPlayer = null;
+ }
+ }
+
+ function start() {
+ if (!SettingsData.lockScreenVideoEnabled || !SettingsData.lockScreenVideoPath)
+ return;
+
+ if (!MultimediaService.available) {
+ ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("QtMultimedia is not available"));
+ return;
+ }
+
+ if (!createVideoPlayer())
+ return;
+
+ videoPicker.result = "";
+ videoPicker.folder = "";
+ inputEnabled = false;
+ mouseInitialized = false;
+ lastMousePos = Qt.point(-1, -1);
+ active = true;
+ inputEnableTimer.start();
+ fileChecker.running = true;
+ }
+
+ function dismiss() {
+ if (!active)
+ return;
+ destroyVideoPlayer();
+ inputEnabled = false;
+ active = false;
+ videoSource = "";
+ dismissed();
+ }
+
+ onVideoSourceChanged: {
+ if (videoSource && active && videoPlayer) {
+ videoPlayer.source = videoSource;
+ videoPlayer.play();
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ enabled: root.active && root.inputEnabled
+ hoverEnabled: true
+ propagateComposedEvents: false
+
+ onPositionChanged: mouse => {
+ if (!root.mouseInitialized) {
+ root.lastMousePos = Qt.point(mouse.x, mouse.y);
+ root.mouseInitialized = true;
+ return;
+ }
+ var dx = Math.abs(mouse.x - root.lastMousePos.x);
+ var dy = Math.abs(mouse.y - root.lastMousePos.y);
+ if (dx > 5 || dy > 5) {
+ root.dismiss();
+ }
+ }
+ onClicked: root.dismiss()
+ onPressed: root.dismiss()
+ onWheel: root.dismiss()
+ }
+
+ Connections {
+ target: IdleService
+
+ function onLockRequested() {
+ if (SettingsData.lockScreenVideoEnabled && !root.active) {
+ root.start();
+ }
+ }
+
+ function onFadeToLockRequested() {
+ if (SettingsData.lockScreenVideoEnabled && !root.active) {
+ IdleService.cancelFadeToLock();
+ root.start();
+ }
+ }
+ }
+}