243 lines
8.7 KiB
QML
243 lines
8.7 KiB
QML
import Quickshell
|
|
import Quickshell.Services.UPower
|
|
import Quickshell.Services.Mpris
|
|
import QtQuick
|
|
import Qt5Compat.GraphicalEffects
|
|
|
|
// Shared appearance; authentication belongs to the greeter or session lock.
|
|
Item {
|
|
id: view
|
|
required property var theme
|
|
property bool busy: false
|
|
property string errorMessage: ""
|
|
signal submitted(string password)
|
|
|
|
Image {
|
|
id: wall
|
|
anchors.fill: parent
|
|
source: "file://" + view.theme.wallImage
|
|
// cache:false — soft reload (qs ipc reload) keeps the process
|
|
// alive, and the QML pixmap cache is keyed by URL, so a new
|
|
// wallpaper behind the same path never shows without this.
|
|
cache: false
|
|
fillMode: Image.PreserveAspectCrop
|
|
visible: false
|
|
}
|
|
FastBlur {
|
|
anchors.fill: wall
|
|
source: wall
|
|
radius: 20
|
|
}
|
|
// Lighter blur means less natural contrast, so the scrim
|
|
// is a left-weighted gradient instead of a flat 0.4 wash:
|
|
// dark under the text column, near-clear on the right.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop { position: 0.0; color: "#b0000000" }
|
|
GradientStop { position: 0.55; color: "#59000000" }
|
|
GradientStop { position: 1.0; color: "#33000000" }
|
|
}
|
|
}
|
|
|
|
SystemClock {
|
|
id: lockClock
|
|
precision: SystemClock.Seconds
|
|
}
|
|
|
|
// Lower-left, not centred: clock/date/password stack left
|
|
// aligned against a 10% margin, sitting below the middle.
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Math.round(parent.width * 0.10)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.verticalCenterOffset: Math.round(parent.height * 0.14)
|
|
spacing: 6
|
|
|
|
Text {
|
|
text: lockClock.date.toLocaleTimeString(Qt.locale(), "HH:mm")
|
|
color: view.theme.base05
|
|
font.family: view.theme.fontFamily
|
|
font.pixelSize: 104
|
|
font.weight: Font.DemiBold
|
|
}
|
|
Text {
|
|
text: lockClock.date.toLocaleDateString(Qt.locale(), "dddd, d MMMM")
|
|
color: view.theme.base04
|
|
font.family: view.theme.fontFamily
|
|
font.pixelSize: 18
|
|
}
|
|
|
|
Item { width: 1; height: 14 }
|
|
|
|
// Power. UPower.displayDevice is the same DBus source
|
|
// the bar reads — no polling, no /sys parsing.
|
|
Row {
|
|
id: lockBatt
|
|
spacing: 8
|
|
property var dev: UPower.displayDevice
|
|
property int level: dev && dev.ready ? Math.round(dev.percentage * 100) : 0
|
|
property bool charging: dev ? dev.state === UPowerDeviceState.Charging : false
|
|
property string remaining: {
|
|
if (!dev) return "";
|
|
let secs = charging ? dev.timeToFull : dev.timeToEmpty;
|
|
if (!secs || secs <= 0) return "";
|
|
return Math.floor(secs / 3600) + "h " + Math.round((secs % 3600) / 60) + "m";
|
|
}
|
|
visible: dev && dev.ready
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: lockBatt.charging ? "battery-charging"
|
|
: lockBatt.level >= 75 ? "battery-full"
|
|
: lockBatt.level >= 50 ? "battery-medium"
|
|
: lockBatt.level >= 20 ? "battery-low"
|
|
: "battery"
|
|
font.family: view.theme.iconFont
|
|
font.pixelSize: 17
|
|
color: lockBatt.level <= 15 ? view.theme.err
|
|
: lockBatt.level <= 30 ? view.theme.warn : view.theme.base04
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: lockBatt.level + "%" + (lockBatt.remaining === "" ? ""
|
|
: " · " + lockBatt.remaining + (lockBatt.charging ? " to full" : " left"))
|
|
color: view.theme.base04
|
|
font.family: view.theme.fontFamily
|
|
font.pixelSize: 15
|
|
}
|
|
}
|
|
|
|
// Now playing. Mpris is a global service, so this shows
|
|
// whatever kept playing into the lock — nothing is woken.
|
|
Row {
|
|
id: lockMedia
|
|
spacing: 8
|
|
property var player: Mpris.players.values.filter(p => p.trackTitle)[0]
|
|
visible: !!player
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "music"
|
|
font.family: view.theme.iconFont
|
|
font.pixelSize: 17
|
|
color: view.theme.base04
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 380
|
|
elide: Text.ElideRight
|
|
text: lockMedia.player
|
|
? lockMedia.player.trackTitle + (lockMedia.player.trackArtist
|
|
? " · " + lockMedia.player.trackArtist : "")
|
|
: ""
|
|
color: view.theme.base04
|
|
font.family: view.theme.fontFamily
|
|
font.pixelSize: 15
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: 14 }
|
|
|
|
|
|
// Password pill: dots for typed chars; border tracks
|
|
// state (accent while checking, red on failure).
|
|
Rectangle {
|
|
width: 300
|
|
height: 46
|
|
radius: view.theme.radiusCard
|
|
color: view.theme.cardBg
|
|
border.width: view.theme.borderWidth
|
|
border.color: view.busy ? view.theme.accent
|
|
: (view.errorMessage !== "") ? view.theme.err
|
|
: view.theme.base03
|
|
Behavior on border.color { ColorAnimation { duration: view.theme.animFade } }
|
|
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: 8
|
|
Repeater {
|
|
model: Math.min(pwInput.text.length, 24)
|
|
Rectangle {
|
|
width: 8; height: 8; radius: 4
|
|
color: view.theme.base05
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: pwInput.text === "" && (view.errorMessage !== "")
|
|
text: view.errorMessage
|
|
color: view.theme.err
|
|
font.family: view.theme.fontFamily
|
|
font.pixelSize: 13
|
|
}
|
|
|
|
TextInput {
|
|
id: pwInput
|
|
anchors.fill: parent
|
|
opacity: 0
|
|
enabled: !view.busy
|
|
echoMode: TextInput.Password
|
|
focus: true
|
|
function go() {
|
|
view.submitted(text);
|
|
text = "";
|
|
}
|
|
// Explicit key handlers: accepted() doesn't
|
|
// fire reliably on the lock surface.
|
|
Keys.onReturnPressed: go()
|
|
Keys.onEnterPressed: go()
|
|
onAccepted: go()
|
|
Keys.onEscapePressed: text = ""
|
|
}
|
|
|
|
// Spinner while PAM checks the password (and, in the greeter,
|
|
// through the fade-out into the session).
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: view.busy
|
|
text: "loader-circle"
|
|
font.family: view.theme.iconFont
|
|
font.pixelSize: 20
|
|
color: view.theme.accent
|
|
RotationAnimation on rotation {
|
|
running: view.busy
|
|
from: 0; to: 360
|
|
duration: 900
|
|
loops: Animation.Infinite
|
|
}
|
|
}
|
|
|
|
// Visible confirm button (after TextInput: on top)
|
|
Rectangle {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 32; height: 32; radius: 16
|
|
color: confirmMa.containsMouse ? view.theme.base02 : "transparent"
|
|
Behavior on color { ColorAnimation { duration: view.theme.animFade } }
|
|
visible: pwInput.text.length > 0
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "arrow-right"
|
|
font.family: view.theme.iconFont
|
|
font.pixelSize: 18
|
|
color: view.theme.accent
|
|
}
|
|
MouseArea {
|
|
id: confirmMa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: pwInput.go()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: pwInput.forceActiveFocus()
|
|
}
|