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 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: {

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