From 524826197545948477e1a8be18a46d67f7f67a0a Mon Sep 17 00:00:00 2001 From: Damocles Date: Sat, 18 Apr 2026 10:04:22 +0200 Subject: [PATCH] split lock surface into LockClock, LockNotifPills, LockWidgets --- shell/lock/LockClock.qml | 65 +++++++++ shell/lock/LockNotifPills.qml | 88 ++++++++++++ shell/lock/LockSurface.qml | 245 +--------------------------------- shell/lock/LockWidgets.qml | 100 ++++++++++++++ shell/lock/qmldir | 3 + 5 files changed, 263 insertions(+), 238 deletions(-) create mode 100644 shell/lock/LockClock.qml create mode 100644 shell/lock/LockNotifPills.qml create mode 100644 shell/lock/LockWidgets.qml diff --git a/shell/lock/LockClock.qml b/shell/lock/LockClock.qml new file mode 100644 index 0000000..b3dba90 --- /dev/null +++ b/shell/lock/LockClock.qml @@ -0,0 +1,65 @@ +import QtQuick +import "../services" as S + +Item { + id: root + + required property real screenHeight + + opacity: 0 + property real _slideX: -80 + + NumberAnimation on opacity { + to: 1 + duration: 400 + easing.type: Easing.OutCubic + } + NumberAnimation on _slideX { + to: 0 + duration: 500 + easing.type: Easing.OutCubic + } + + transform: Translate { + x: root._slideX + } + + Column { + anchors.centerIn: parent + spacing: 8 + rotation: -90 + transformOrigin: Item.Center + + Text { + id: _clockText + text: Qt.formatTime(new Date(), "HH:mm") + color: S.Theme.base05 + font.pixelSize: Math.max(48, root.screenHeight * 0.28) + font.family: S.Theme.fontFamily + font.bold: true + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm") + } + } + + Text { + text: Qt.formatDate(new Date(), "dddd, d MMMM") + color: S.Theme.base04 + font.pixelSize: S.Theme.fontSize + 2 + font.family: S.Theme.fontFamily + + Timer { + interval: 60000 + running: true + repeat: true + onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM") + } + } + } + + implicitWidth: _clockText.height +} diff --git a/shell/lock/LockNotifPills.qml b/shell/lock/LockNotifPills.qml new file mode 100644 index 0000000..35cf4ad --- /dev/null +++ b/shell/lock/LockNotifPills.qml @@ -0,0 +1,88 @@ +import QtQuick +import Quickshell +import "../services" as S + +Row { + id: root + + spacing: 6 + visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0 + + readonly property var _notifGroups: { + const notifs = S.NotifService.list.filter(n => n.state !== "dismissed"); + const groups = {}; + for (const n of notifs) { + const key = n.appIcon || n.appName || "unknown"; + if (!groups[key]) + groups[key] = { + icon: n.appIcon, + name: n.appName, + count: 0 + }; + groups[key].count++; + } + return Object.values(groups); + } + + Repeater { + model: root._notifGroups + + delegate: Rectangle { + id: _pill + required property var modelData + width: _pillRow.implicitWidth + 12 + height: 24 + radius: 12 + color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) + border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) + border.width: 1 + + HoverHandler { + id: _pillHover + } + + // App name tooltip + Text { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.top + anchors.bottomMargin: 4 + text: _pill.modelData.name || "" + color: S.Theme.base04 + font.pixelSize: S.Theme.fontSize - 2 + font.family: S.Theme.fontFamily + visible: _pillHover.hovered && text !== "" + } + + Row { + id: _pillRow + anchors.centerIn: parent + spacing: 4 + + Image { + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + source: { + const icon = _pill.modelData.icon; + if (!icon) + return ""; + if (icon.startsWith("/")) + return icon; + return Quickshell.iconPath(icon) ?? ""; + } + sourceSize: Qt.size(14, 14) + visible: source !== "" + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: _pill.modelData.count > 1 ? _pill.modelData.count.toString() : "" + color: S.Theme.base04 + font.pixelSize: S.Theme.fontSize - 2 + font.family: S.Theme.fontFamily + visible: _pill.modelData.count > 1 + } + } + } + } +} diff --git a/shell/lock/LockSurface.qml b/shell/lock/LockSurface.qml index bde96a9..757c6ee 100644 --- a/shell/lock/LockSurface.qml +++ b/shell/lock/LockSurface.qml @@ -2,8 +2,6 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland -import Quickshell.Services.Mpris -import Quickshell.Services.Pipewire import "../services" as S import "../applets" as C @@ -47,7 +45,6 @@ WlSessionLockSurface { // Hex wave overlay C.HexWaveBackground { - id: hexWave anchors.fill: parent running: root.lock.secure opacity: root._bgOpacity * 0.4 @@ -83,74 +80,17 @@ WlSessionLockSurface { } } - // Clock - rotated, left-aligned, scaled to screen height - Item { + // Clock - rotated, left-aligned + LockClock { id: _clockItem anchors.left: parent.left anchors.leftMargin: 48 anchors.top: parent.top anchors.bottom: parent.bottom - width: _clockText.height - - opacity: 0 - property real _slideX: -80 - - ParallelAnimation on opacity { - NumberAnimation { - to: 1 - duration: 400 - easing.type: Easing.OutCubic - } - } - NumberAnimation on _slideX { - to: 0 - duration: 500 - easing.type: Easing.OutCubic - } - - transform: Translate { - x: _clockItem._slideX - } - - Column { - id: _clockCol - anchors.centerIn: parent - spacing: 8 - rotation: -90 - transformOrigin: Item.Center - Text { - id: _clockText - text: Qt.formatTime(new Date(), "HH:mm") - color: S.Theme.base05 - font.pixelSize: Math.max(48, root.height * 0.28) - font.family: S.Theme.fontFamily - font.bold: true - - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm") - } - } - - Text { - text: Qt.formatDate(new Date(), "dddd, d MMMM") - color: S.Theme.base04 - font.pixelSize: S.Theme.fontSize + 2 - font.family: S.Theme.fontFamily - - Timer { - interval: 60000 - running: true - repeat: true - onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM") - } - } - } + screenHeight: root.height } - // Center content + // Center content - password and notifications Item { id: content anchors.centerIn: parent @@ -175,89 +115,8 @@ WlSessionLockSurface { anchors.horizontalCenter: parent.horizontalCenter spacing: 24 - // Notification pills - Row { + LockNotifPills { anchors.horizontalCenter: parent.horizontalCenter - spacing: 6 - visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0 - - readonly property var _notifGroups: { - const notifs = S.NotifService.list.filter(n => n.state !== "dismissed"); - const groups = {}; - for (const n of notifs) { - const key = n.appIcon || n.appName || "unknown"; - if (!groups[key]) - groups[key] = { - icon: n.appIcon, - name: n.appName, - count: 0 - }; - groups[key].count++; - } - return Object.values(groups); - } - - Repeater { - model: parent._notifGroups - - delegate: Rectangle { - id: _pill - required property var modelData - width: _pillRow.implicitWidth + 12 - height: 24 - radius: 12 - color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) - border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) - border.width: 1 - - HoverHandler { - id: _pillHover - } - - // App name tooltip - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.top - anchors.bottomMargin: 4 - text: _pill.modelData.name || "" - color: S.Theme.base04 - font.pixelSize: S.Theme.fontSize - 2 - font.family: S.Theme.fontFamily - visible: _pillHover.hovered && text !== "" - } - - Row { - id: _pillRow - anchors.centerIn: parent - spacing: 4 - - Image { - anchors.verticalCenter: parent.verticalCenter - width: 14 - height: 14 - source: { - const icon = _pill.modelData.icon; - if (!icon) - return ""; - if (icon.startsWith("/")) - return icon; - return Quickshell.iconPath(icon) ?? ""; - } - sourceSize: Qt.size(14, 14) - visible: source !== "" - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: _pill.modelData.count > 1 ? _pill.modelData.count.toString() : "" - color: S.Theme.base04 - font.pixelSize: S.Theme.fontSize - 2 - font.family: S.Theme.fontFamily - visible: _pill.modelData.count > 1 - } - } - } - } } // Spacer @@ -276,7 +135,6 @@ WlSessionLockSurface { // Error message Text { - id: _errorText anchors.horizontalCenter: parent.horizontalCenter text: root.auth.message color: S.Theme.base08 @@ -300,101 +158,12 @@ WlSessionLockSurface { } } - // Right column - widgets, slides in from right - Item { + // Right column - widgets + LockWidgets { id: _widgetCol anchors.right: parent.right anchors.rightMargin: 48 anchors.verticalCenter: parent.verticalCenter - width: 280 - height: _widgetContent.implicitHeight - visible: _mprisCard.visible || _volumeCard.visible - - opacity: 0 - property real _slideX: 80 - - NumberAnimation on opacity { - to: 1 - duration: 400 - easing.type: Easing.OutCubic - } - NumberAnimation on _slideX { - to: 0 - duration: 500 - easing.type: Easing.OutCubic - } - - transform: Translate { - x: _widgetCol._slideX - } - - Column { - id: _widgetContent - width: parent.width - spacing: 12 - - // Media widget - Rectangle { - id: _mprisCard - width: parent.width - height: _mprisContent.implicitHeight + 16 - radius: S.Theme.radius + 2 - color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) - border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) - border.width: 1 - visible: (S.Modules.lock.mpris ?? true) && _mprisPlayer !== null - - readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) - property int _playerIdx: 0 - readonly property var _mprisPlayer: _mprisPlayers[_playerIdx] ?? _mprisPlayers[0] ?? null - readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing - - C.MprisApplet { - id: _mprisContent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - player: _mprisCard._mprisPlayer - players: _mprisCard._mprisPlayers - playing: _mprisCard._playing - playerIdx: _mprisCard._playerIdx - accentColor: S.Theme.base0D - cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" - onPlayerSwitched: idx => { - _mprisCard._playerIdx = idx; - } - } - } - - // Volume widget - Rectangle { - id: _volumeCard - width: parent.width - height: _volumeContent.implicitHeight + 16 - radius: S.Theme.radius + 2 - color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) - border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) - border.width: 1 - visible: (S.Modules.lock.volume ?? true) && Pipewire.defaultAudioSink !== null - - PwObjectTracker { - objects: [Pipewire.defaultAudioSink] - } - - C.VolumeApplet { - id: _volumeContent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - sink: Pipewire.defaultAudioSink - sinkList: [] - streamList: [] - accentColor: S.Theme.base0E - } - } - } } onVisibleChanged: { diff --git a/shell/lock/LockWidgets.qml b/shell/lock/LockWidgets.qml new file mode 100644 index 0000000..1818cad --- /dev/null +++ b/shell/lock/LockWidgets.qml @@ -0,0 +1,100 @@ +import QtQuick +import Quickshell.Services.Mpris +import Quickshell.Services.Pipewire +import "../services" as S +import "../applets" as C + +Item { + id: root + + width: 280 + + opacity: 0 + property real _slideX: 80 + + NumberAnimation on opacity { + to: 1 + duration: 400 + easing.type: Easing.OutCubic + } + NumberAnimation on _slideX { + to: 0 + duration: 500 + easing.type: Easing.OutCubic + } + + transform: Translate { + x: root._slideX + } + + implicitHeight: _widgetContent.implicitHeight + visible: _mprisCard.visible || _volumeCard.visible + + Column { + id: _widgetContent + width: parent.width + spacing: 12 + + // Media widget + Rectangle { + id: _mprisCard + width: parent.width + height: _mprisContent.implicitHeight + 16 + radius: S.Theme.radius + 2 + color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) + border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) + border.width: 1 + visible: (S.Modules.lock.mpris ?? true) && _mprisPlayer !== null + + readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) + property int _playerIdx: 0 + readonly property var _mprisPlayer: _mprisPlayers[_playerIdx] ?? _mprisPlayers[0] ?? null + readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing + + C.MprisApplet { + id: _mprisContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + player: _mprisCard._mprisPlayer + players: _mprisCard._mprisPlayers + playing: _mprisCard._playing + playerIdx: _mprisCard._playerIdx + accentColor: S.Theme.base0D + cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" + onPlayerSwitched: idx => { + _mprisCard._playerIdx = idx; + } + } + } + + // Volume widget + Rectangle { + id: _volumeCard + width: parent.width + height: _volumeContent.implicitHeight + 16 + radius: S.Theme.radius + 2 + color: Qt.rgba(S.Theme.base01.r, S.Theme.base01.g, S.Theme.base01.b, 0.7) + border.color: Qt.rgba(S.Theme.base03.r, S.Theme.base03.g, S.Theme.base03.b, 0.3) + border.width: 1 + visible: (S.Modules.lock.volume ?? true) && Pipewire.defaultAudioSink !== null + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + C.VolumeApplet { + id: _volumeContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + sink: Pipewire.defaultAudioSink + sinkList: [] + streamList: [] + accentColor: S.Theme.base0E + } + } + } +} diff --git a/shell/lock/qmldir b/shell/lock/qmldir index 217d967..ea9289b 100644 --- a/shell/lock/qmldir +++ b/shell/lock/qmldir @@ -1,4 +1,7 @@ Lock 1.0 Lock.qml LockAuth 1.0 LockAuth.qml +LockClock 1.0 LockClock.qml LockInput 1.0 LockInput.qml +LockNotifPills 1.0 LockNotifPills.qml LockSurface 1.0 LockSurface.qml +LockWidgets 1.0 LockWidgets.qml