493 lines
15 KiB
QML
493 lines
15 KiB
QML
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
|
|
import "../modules" as M
|
|
|
|
WlSessionLockSurface {
|
|
id: root
|
|
|
|
required property WlSessionLock lock
|
|
required property LockAuth auth
|
|
|
|
color: S.Theme.base00
|
|
|
|
property real _bgOpacity: 0
|
|
NumberAnimation on _bgOpacity {
|
|
to: 1
|
|
duration: 400
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
|
|
// Blur screenshot of desktop as background
|
|
ScreencopyView {
|
|
anchors.fill: parent
|
|
captureSource: root.screen
|
|
opacity: root._bgOpacity
|
|
visible: S.Modules.lock.screenshot ?? true
|
|
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
autoPaddingEnabled: false
|
|
blurEnabled: true
|
|
blur: 1
|
|
blurMax: 64
|
|
}
|
|
}
|
|
|
|
// Dim overlay
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Qt.rgba(S.Theme.base00.r, S.Theme.base00.g, S.Theme.base00.b, 0.4)
|
|
opacity: root._bgOpacity
|
|
}
|
|
|
|
// Hex wave overlay
|
|
C.HexWaveBackground {
|
|
id: hexWave
|
|
anchors.fill: parent
|
|
running: root.lock.secure
|
|
opacity: root._bgOpacity * 0.4
|
|
}
|
|
|
|
// 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.
|
|
// Declared before content so it's below in z-order - content TapHandlers
|
|
// receive mouse events, while keyboard activeFocus is independent of stacking.
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 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: S.Theme.base05
|
|
font.pixelSize: 72
|
|
font.family: S.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: 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")
|
|
}
|
|
}
|
|
|
|
// Notification pills
|
|
Row {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: 6
|
|
visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0
|
|
|
|
readonly property var _notifGroups: {
|
|
const notifs = M.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 {
|
|
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
|
|
|
|
Row {
|
|
id: _pillRow
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
|
|
Image {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 14
|
|
height: 14
|
|
source: {
|
|
const icon = 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: modelData.count > 1 ? modelData.count.toString() : ""
|
|
color: S.Theme.base04
|
|
font.pixelSize: S.Theme.fontSize - 2
|
|
font.family: S.Theme.fontFamily
|
|
visible: modelData.count > 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: S.Theme.base08
|
|
font.pixelSize: S.Theme.fontSize - 1
|
|
font.family: S.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: 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
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: 280
|
|
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: {
|
|
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: root
|
|
property: "_bgOpacity"
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Screen corners - session lock surfaces render above all layer-shell,
|
|
// so the bar's ScreenCorners aren't visible. Draw our own.
|
|
component LockCorner: Canvas {
|
|
property int corner: 0
|
|
readonly property int _r: S.Theme.screenRadius
|
|
visible: _r > 0 && S.Modules.screenCorners.enable
|
|
width: _r
|
|
height: _r
|
|
z: 999
|
|
|
|
onPaint: {
|
|
const r = _r;
|
|
const ctx = getContext("2d");
|
|
ctx.clearRect(0, 0, r, r);
|
|
ctx.fillStyle = "black";
|
|
ctx.beginPath();
|
|
switch (corner) {
|
|
case 0:
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(r, 0);
|
|
ctx.arc(r, r, r, -Math.PI / 2, Math.PI, true);
|
|
ctx.closePath();
|
|
break;
|
|
case 1:
|
|
ctx.moveTo(r, 0);
|
|
ctx.lineTo(0, 0);
|
|
ctx.arc(0, r, r, -Math.PI / 2, 0, false);
|
|
ctx.closePath();
|
|
break;
|
|
case 2:
|
|
ctx.moveTo(0, r);
|
|
ctx.lineTo(0, 0);
|
|
ctx.arc(r, 0, r, Math.PI, Math.PI / 2, true);
|
|
ctx.closePath();
|
|
break;
|
|
case 3:
|
|
ctx.moveTo(r, r);
|
|
ctx.lineTo(r, 0);
|
|
ctx.arc(0, 0, r, 0, Math.PI / 2, false);
|
|
ctx.closePath();
|
|
break;
|
|
}
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
LockCorner {
|
|
corner: 0
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
}
|
|
LockCorner {
|
|
corner: 1
|
|
anchors.top: parent.top
|
|
anchors.right: parent.right
|
|
}
|
|
LockCorner {
|
|
corner: 2
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
}
|
|
LockCorner {
|
|
corner: 3
|
|
anchors.bottom: parent.bottom
|
|
anchors.right: parent.right
|
|
}
|
|
}
|