diff options
| author | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
|---|---|---|
| committer | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
| commit | 5d94c0a7d44a2255b81815a52a7056a94a39842d (patch) | |
| tree | 759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock | |
| parent | e79cdf210b267f21a186255ce1a4d50029439d54 (diff) | |
| download | RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip | |
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock')
12 files changed, 4176 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/CustomButtonKeyboard.qml new file mode 100644 index 0000000..639389c --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToDpmsWindow.qml new file mode 100644 index 0000000..0486890 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/FadeToLockWindow.qml new file mode 100644 index 0000000..c73a59a --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Keyboard.qml new file mode 100644 index 0000000..cfc0c61 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/KeyboardController.qml new file mode 100644 index 0000000..09d77a5 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Lock.qml new file mode 100644 index 0000000..9a5a00f --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockPowerMenu.qml new file mode 100644 index 0000000..0a0ee84 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenContent.qml new file mode 100644 index 0000000..b296e98 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockScreenDemo.qml new file mode 100644 index 0000000..e13eac9 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/LockSurface.qml new file mode 100644 index 0000000..9d1beae --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/Pam.qml new file mode 100644 index 0000000..f5e7859 --- /dev/null +++ b/raveos-theme/hyprland/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-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Lock/VideoScreensaver.qml new file mode 100644 index 0000000..129f507 --- /dev/null +++ b/raveos-theme/hyprland/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(); + } + } + } +} |