split lock surface into LockClock, LockNotifPills, LockWidgets

This commit is contained in:
Damocles 2026-04-18 10:04:22 +02:00
parent de193a88cd
commit 5248261975
5 changed files with 263 additions and 238 deletions

65
shell/lock/LockClock.qml Normal file
View file

@ -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
}

View file

@ -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
}
}
}
}
}

View file

@ -2,8 +2,6 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import "../services" as S import "../services" as S
import "../applets" as C import "../applets" as C
@ -47,7 +45,6 @@ WlSessionLockSurface {
// Hex wave overlay // Hex wave overlay
C.HexWaveBackground { C.HexWaveBackground {
id: hexWave
anchors.fill: parent anchors.fill: parent
running: root.lock.secure running: root.lock.secure
opacity: root._bgOpacity * 0.4 opacity: root._bgOpacity * 0.4
@ -83,74 +80,17 @@ WlSessionLockSurface {
} }
} }
// Clock - rotated, left-aligned, scaled to screen height // Clock - rotated, left-aligned
Item { LockClock {
id: _clockItem id: _clockItem
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 48 anchors.leftMargin: 48
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: _clockText.height screenHeight: root.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 { // Center content - password and notifications
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")
}
}
}
}
// Center content
Item { Item {
id: content id: content
anchors.centerIn: parent anchors.centerIn: parent
@ -175,89 +115,8 @@ WlSessionLockSurface {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: 24 spacing: 24
// Notification pills LockNotifPills {
Row {
anchors.horizontalCenter: parent.horizontalCenter 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 // Spacer
@ -276,7 +135,6 @@ WlSessionLockSurface {
// Error message // Error message
Text { Text {
id: _errorText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: root.auth.message text: root.auth.message
color: S.Theme.base08 color: S.Theme.base08
@ -300,101 +158,12 @@ WlSessionLockSurface {
} }
} }
// Right column - widgets, slides in from right // Right column - widgets
Item { LockWidgets {
id: _widgetCol id: _widgetCol
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 48 anchors.rightMargin: 48
anchors.verticalCenter: parent.verticalCenter 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: { onVisibleChanged: {

100
shell/lock/LockWidgets.qml Normal file
View file

@ -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
}
}
}
}

View file

@ -1,4 +1,7 @@
Lock 1.0 Lock.qml Lock 1.0 Lock.qml
LockAuth 1.0 LockAuth.qml LockAuth 1.0 LockAuth.qml
LockClock 1.0 LockClock.qml
LockInput 1.0 LockInput.qml LockInput 1.0 LockInput.qml
LockNotifPills 1.0 LockNotifPills.qml
LockSurface 1.0 LockSurface.qml LockSurface 1.0 LockSurface.qml
LockWidgets 1.0 LockWidgets.qml