nova-shell/shell/lock/LockSurface.qml
2026-04-17 21:58:48 +02:00

364 lines
10 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import "../modules" as M
import "../applets" as C
WlSessionLockSurface {
id: root
required property WlSessionLock lock
required property LockAuth auth
color: M.Theme.base00
// Blur screenshot of desktop as background
ScreencopyView {
id: background
anchors.fill: parent
captureSource: root.screen
opacity: 0
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 1
blurMax: 64
}
NumberAnimation on opacity {
to: 1
duration: 400
easing.type: Easing.OutCubic
}
}
// Dim overlay
Rectangle {
anchors.fill: parent
color: Qt.rgba(M.Theme.base00.r, M.Theme.base00.g, M.Theme.base00.b, 0.4)
opacity: background.opacity
}
// Hex wave overlay
C.HexWaveBackground {
id: hexWave
anchors.fill: parent
running: root.lock.secure
opacity: background.opacity * 0.4
NumberAnimation on opacity {
id: _hexFadeIn
running: false
to: 0.4
duration: 600
easing.type: Easing.OutCubic
}
}
// Center content
Item {
id: content
anchors.centerIn: parent
width: 320
height: _col.height
opacity: 0
scale: 0.9
NumberAnimation on opacity {
to: 1
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation on scale {
to: 1
duration: 300
easing.type: Easing.OutCubic
}
Column {
id: _col
anchors.horizontalCenter: parent.horizontalCenter
spacing: 24
// Clock
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatTime(new Date(), "HH:mm")
color: M.Theme.base05
font.pixelSize: 72
font.family: M.Theme.fontFamily
font.bold: true
Timer {
interval: 1000
running: true
repeat: true
onTriggered: parent.text = Qt.formatTime(new Date(), "HH:mm")
}
}
// Date
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDate(new Date(), "dddd, d MMMM")
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.fontFamily
Timer {
interval: 60000
running: true
repeat: true
onTriggered: parent.text = Qt.formatDate(new Date(), "dddd, d MMMM")
}
}
// Spacer
Item {
width: 1
height: 24
}
// Password input
LockInput {
anchors.horizontalCenter: parent.horizontalCenter
width: 280
buffer: root.auth.buffer
state: root.auth.state
}
// Error message
Text {
id: _errorText
anchors.horizontalCenter: parent.horizontalCenter
text: root.auth.message
color: M.Theme.base08
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
opacity: root.auth.message ? 1 : 0
height: root.auth.message ? implicitHeight : 0
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
// Spacer before widgets
Item {
width: 1
height: 8
visible: _mprisCard.visible || _volumeCard.visible
}
// Media widget
Rectangle {
id: _mprisCard
anchors.horizontalCenter: parent.horizontalCenter
width: 280
height: _mprisContent.implicitHeight + 16
radius: M.Theme.radius + 2
color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7)
border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3)
border.width: 1
visible: _mprisPlayer !== null
readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused)
readonly property var _mprisPlayer: _mprisPlayers[0] ?? null
readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing
C.MprisContent {
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
accentColor: M.Theme.base0D
cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? ""
}
}
// Volume widget
Rectangle {
id: _volumeCard
anchors.horizontalCenter: parent.horizontalCenter
width: 280
height: _volumeContent.implicitHeight + 16
radius: M.Theme.radius + 2
color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7)
border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3)
border.width: 1
visible: Pipewire.defaultAudioSink !== null
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
C.VolumeContent {
id: _volumeContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 8
sink: Pipewire.defaultAudioSink
sinkList: []
streamList: []
accentColor: M.Theme.base0E
}
}
}
}
// Keyboard input via TextInput - engages Qt's full input pipeline including
// text-input protocol, which is more reliable than Keys on a plain Item in
// layer-shell/lock surfaces where raw wl_keyboard delivery can be flaky.
TextInput {
id: _keyInput
anchors.fill: parent
focus: true
color: "transparent"
selectionColor: "transparent"
selectedTextColor: "transparent"
cursorVisible: false
enabled: !root._unlocking && root.auth.state !== "max" && root.auth.state !== "busy"
onTextChanged: if (root.auth)
root.auth.buffer = text
Keys.onReturnPressed: root.auth.submit()
Keys.onEnterPressed: root.auth.submit()
Keys.onEscapePressed: {
text = "";
}
onActiveFocusChanged: {
if (!activeFocus)
forceActiveFocus();
}
}
onVisibleChanged: {
if (visible)
_keyInput.forceActiveFocus();
}
// Sync TextInput when auth clears buffer externally (PAM submit, lock reset)
Connections {
target: root.auth
function onBufferChanged() {
if (_keyInput.text !== root.auth.buffer)
_keyInput.text = root.auth.buffer;
}
}
// Unlock animation
property bool _unlocking: false
Connections {
target: root.auth
function onUnlockRequested() {
root._unlocking = true;
_unlockAnim.start();
}
}
SequentialAnimation {
id: _unlockAnim
ParallelAnimation {
NumberAnimation {
target: content
property: "opacity"
to: 0
duration: 200
easing.type: Easing.InCubic
}
NumberAnimation {
target: content
property: "scale"
to: 0.9
duration: 200
easing.type: Easing.InCubic
}
NumberAnimation {
target: background
property: "opacity"
to: 0
duration: 300
easing.type: Easing.InCubic
}
NumberAnimation {
target: hexWave
property: "opacity"
to: 0
duration: 300
easing.type: Easing.InCubic
}
}
PropertyAction {
target: root.lock
property: "locked"
value: false
}
}
// Shake animation on auth failure
SequentialAnimation {
id: _shakeAnim
NumberAnimation {
target: content
property: "anchors.horizontalCenterOffset"
to: 12
duration: 50
}
NumberAnimation {
target: content
property: "anchors.horizontalCenterOffset"
to: -12
duration: 50
}
NumberAnimation {
target: content
property: "anchors.horizontalCenterOffset"
to: 8
duration: 50
}
NumberAnimation {
target: content
property: "anchors.horizontalCenterOffset"
to: -8
duration: 50
}
NumberAnimation {
target: content
property: "anchors.horizontalCenterOffset"
to: 0
duration: 50
}
}
Connections {
target: root.auth
function onAuthFailed() {
_shakeAnim.restart();
}
}
}