Compare commits
8 changed files with 277 additions and 384 deletions
|
|
@ -1,11 +1,15 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import "../services" as S
|
import "../services" as S
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool _enabled: S.Modules.lock.enable
|
||||||
|
property string _sessionPath: ""
|
||||||
|
|
||||||
WlSessionLock {
|
WlSessionLock {
|
||||||
id: _lock
|
id: _lock
|
||||||
|
|
||||||
|
|
@ -20,30 +24,73 @@ Scope {
|
||||||
lock: _lock
|
lock: _lock
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
// Resolve the actual logind session object path at startup
|
||||||
target: S.LockService
|
Process {
|
||||||
|
id: _sessionResolver
|
||||||
|
command: ["busctl", "call", "--system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetSession", "s", "auto"]
|
||||||
|
running: root._enabled
|
||||||
|
|
||||||
function onLockRequested() {
|
stdout: SplitParser {
|
||||||
if (S.LockService.enabled)
|
onRead: data => {
|
||||||
_lock.locked = true;
|
// Output: o "/org/freedesktop/login1/session/_32"
|
||||||
|
const match = data.match(/"([^"]+)"/);
|
||||||
|
if (match)
|
||||||
|
root._sessionPath = match[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnlockRequested() {
|
onExited: {
|
||||||
if (_lock.locked)
|
if (root._sessionPath) {
|
||||||
_lock.locked = false;
|
_logindMonitor.running = true;
|
||||||
|
_lockStateCheck.running = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSessionLocked() {
|
// Listen for logind Lock/Unlock signals via gdbus monitor.
|
||||||
if (S.LockService.enabled && !_lock.locked)
|
// TODO: replace with native D-Bus integration when nova-stats becomes a quickshell plugin
|
||||||
_lock.locked = true;
|
Process {
|
||||||
|
id: _logindMonitor
|
||||||
|
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root._sessionPath]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
if (data.indexOf(".Lock ()") !== -1 && root._enabled)
|
||||||
|
_lock.locked = true;
|
||||||
|
else if (data.indexOf(".Unlock ()") !== -1 && _lock.locked)
|
||||||
|
_lock.locked = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if session is already locked on startup (crash recovery)
|
||||||
|
Process {
|
||||||
|
id: _lockStateCheck
|
||||||
|
running: false
|
||||||
|
command: ["busctl", "get-property", "--system", "org.freedesktop.login1", root._sessionPath, "org.freedesktop.login1.Session", "LockedHint"]
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
// Output: b true/false
|
||||||
|
if (data.indexOf("true") !== -1 && root._enabled && !_lock.locked)
|
||||||
|
_lock.locked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logind LockedHint when lock state changes
|
||||||
|
Process {
|
||||||
|
id: _lockedHint
|
||||||
|
command: ["busctl", "call", "--system", "org.freedesktop.login1", root._sessionPath || "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetLockedHint", "b", _lock.locked ? "true" : "false"]
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: _lock
|
target: _lock
|
||||||
|
|
||||||
function onLockStateChanged() {
|
function onLockStateChanged() {
|
||||||
S.LockService.setLockedHint(_lock.locked);
|
if (root._sessionPath)
|
||||||
|
_lockedHint.running = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,8 @@ 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
|
||||||
|
|
||||||
|
|
@ -45,6 +47,7 @@ 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
|
||||||
|
|
@ -80,17 +83,74 @@ WlSessionLockSurface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock - rotated, left-aligned
|
// Clock - rotated, left-aligned, scaled to screen height
|
||||||
LockClock {
|
Item {
|
||||||
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
|
||||||
screenHeight: root.height
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center content - password and notifications
|
// Center content
|
||||||
Item {
|
Item {
|
||||||
id: content
|
id: content
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
@ -115,8 +175,89 @@ WlSessionLockSurface {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: 24
|
spacing: 24
|
||||||
|
|
||||||
LockNotifPills {
|
// Notification pills
|
||||||
|
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
|
||||||
|
|
@ -135,6 +276,7 @@ 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
|
||||||
|
|
@ -155,15 +297,78 @@ WlSessionLockSurface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right column - widgets
|
// Spacer before widgets
|
||||||
LockWidgets {
|
Item {
|
||||||
id: _widgetCol
|
width: 1
|
||||||
anchors.right: parent.right
|
height: 8
|
||||||
anchors.rightMargin: 48
|
visible: _mprisCard.visible || _volumeCard.visible
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
}
|
||||||
|
|
||||||
|
// 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: {
|
onVisibleChanged: {
|
||||||
|
|
@ -225,20 +430,6 @@ WlSessionLockSurface {
|
||||||
duration: 200
|
duration: 200
|
||||||
easing.type: Easing.InCubic
|
easing.type: Easing.InCubic
|
||||||
}
|
}
|
||||||
NumberAnimation {
|
|
||||||
target: _widgetCol
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: _widgetCol
|
|
||||||
property: "_slideX"
|
|
||||||
to: 80
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: root
|
target: root
|
||||||
property: "_bgOpacity"
|
property: "_bgOpacity"
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
import "." as S
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property bool enabled: S.Modules.lock.enable
|
|
||||||
property string sessionPath: ""
|
|
||||||
|
|
||||||
// Lock/unlock requests from logind
|
|
||||||
signal lockRequested
|
|
||||||
signal unlockRequested
|
|
||||||
|
|
||||||
// Set logind LockedHint
|
|
||||||
function setLockedHint(locked) {
|
|
||||||
if (!sessionPath)
|
|
||||||
return;
|
|
||||||
_lockedHint._locked = locked;
|
|
||||||
_lockedHint.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if session is currently locked (crash recovery)
|
|
||||||
function checkLockState() {
|
|
||||||
if (sessionPath)
|
|
||||||
_lockStateCheck.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
signal sessionLocked // fired if LockedHint is true on startup
|
|
||||||
|
|
||||||
// Resolve the actual logind session object path at startup
|
|
||||||
property Process _sessionResolver: Process {
|
|
||||||
command: ["busctl", "call", "--system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetSession", "s", "auto"]
|
|
||||||
running: root.enabled
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
const match = data.match(/"([^"]+)"/);
|
|
||||||
if (match)
|
|
||||||
root.sessionPath = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: {
|
|
||||||
if (root.sessionPath) {
|
|
||||||
_logindMonitor.running = true;
|
|
||||||
root.checkLockState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for logind Lock/Unlock signals via gdbus monitor.
|
|
||||||
// TODO: replace with native D-Bus integration when nova-stats becomes a quickshell plugin
|
|
||||||
property Process _logindMonitor: Process {
|
|
||||||
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
if (data.indexOf(".Lock ()") !== -1)
|
|
||||||
root.lockRequested();
|
|
||||||
else if (data.indexOf(".Unlock ()") !== -1)
|
|
||||||
root.unlockRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if session is already locked on startup (crash recovery)
|
|
||||||
property Process _lockStateCheck: Process {
|
|
||||||
running: false
|
|
||||||
command: ["busctl", "get-property", "--system", "org.freedesktop.login1", root.sessionPath, "org.freedesktop.login1.Session", "LockedHint"]
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
if (data.indexOf("true") !== -1)
|
|
||||||
root.sessionLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set logind LockedHint
|
|
||||||
property Process _lockedHint: Process {
|
|
||||||
property bool _locked: false
|
|
||||||
command: ["busctl", "call", "--system", "org.freedesktop.login1", root.sessionPath || "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetLockedHint", "b", _locked ? "true" : "false"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,4 +6,3 @@ singleton NiriIpc 1.0 NiriIpc.qml
|
||||||
singleton PowerProfileService 1.0 PowerProfileService.qml
|
singleton PowerProfileService 1.0 PowerProfileService.qml
|
||||||
singleton NotifService 1.0 NotifService.qml
|
singleton NotifService 1.0 NotifService.qml
|
||||||
NotifItem 1.0 NotifItem.qml
|
NotifItem 1.0 NotifItem.qml
|
||||||
singleton LockService 1.0 LockService.qml
|
|
||||||
|
|
|
||||||
Reference in a new issue