feat(nosignal): redesign lock screen to match handoff
Rewrite the WlSessionLockSurface visuals to the NoSignal lock design while keeping the secure session-lock + PAM machinery (Pam buffer/handleKey/state, WlSessionLock unlock): - Blurred screencopy wallpaper + dark scrim; top status row (lock+Locked / wifi/bt/battery); centred 108px ExtraLight clock + date; gradient avatar + username; password field (key glyph, masked dots, peach enter glyph, error border) fed to pam.handleKey; bottom now-playing strip + "Press Enter to unlock". JetBrains Mono / Nerd Font (NsIcon) / Theme tokens. - Escape does not dismiss; correct password (PAM) unlocks; wrong password shows inline error. Verified in VM: renders, masks, unlocks. - Glyphs: add login (enter arrow) + key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83d5de1ba3
commit
7c06b808ae
2 changed files with 264 additions and 169 deletions
|
|
@ -1,11 +1,22 @@
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
// NoSignal lock screen surface (interface redesign 2026-06-14).
|
||||||
|
// Secure ext-session-lock content: blurred wallpaper + scrim, top status row,
|
||||||
|
// centred clock/date/avatar/username/password, bottom now-playing strip.
|
||||||
|
// Reuses caelestia's Pam (buffer + handleKey + state) for PAM auth and the
|
||||||
|
// WlSessionLock unlock signal — input is fed to pam.handleKey, never compared
|
||||||
|
// in QML. JetBrains Mono / Nerd Font / NoSignal Theme tokens.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import Quickshell.Services.UPower
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.services
|
import qs.services
|
||||||
|
import qs.utils
|
||||||
|
|
||||||
WlSessionLockSurface {
|
WlSessionLockSurface {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -13,7 +24,7 @@ WlSessionLockSurface {
|
||||||
required property WlSessionLock lock
|
required property WlSessionLock lock
|
||||||
required property Pam pam
|
required property Pam pam
|
||||||
|
|
||||||
readonly property alias unlocking: unlockAnim.running
|
readonly property bool errored: root.pam.state === "error" || root.pam.state === "fail"
|
||||||
|
|
||||||
contentItem.Config.screen: screen.name
|
contentItem.Config.screen: screen.name
|
||||||
contentItem.Tokens.screen: screen.name
|
contentItem.Tokens.screen: screen.name
|
||||||
|
|
@ -22,144 +33,17 @@ WlSessionLockSurface {
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onUnlock(): void {
|
function onUnlock(): void {
|
||||||
unlockAnim.start();
|
root.lock.locked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root.lock
|
target: root.lock
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
// ── Blurred wallpaper + scrim ──────────────────────────────────────────
|
||||||
id: unlockAnim
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
Anim {
|
|
||||||
target: lockContent
|
|
||||||
properties: "implicitWidth,implicitHeight"
|
|
||||||
to: lockContent.size
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockBg
|
|
||||||
property: "radius"
|
|
||||||
to: lockContent.radius
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: content
|
|
||||||
property: "scale"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: content
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
type: Anim.StandardSmall
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockIcon
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
type: Anim.StandardLarge
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: background
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
type: Anim.StandardLarge
|
|
||||||
}
|
|
||||||
SequentialAnimation {
|
|
||||||
PauseAnimation {
|
|
||||||
duration: Tokens.anim.durations.small
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
type: Anim.Standard
|
|
||||||
target: lockContent
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PropertyAction {
|
|
||||||
target: root.lock
|
|
||||||
property: "locked"
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
id: initAnim
|
|
||||||
|
|
||||||
running: true
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
target: background
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
type: Anim.StandardLarge
|
|
||||||
}
|
|
||||||
SequentialAnimation {
|
|
||||||
ParallelAnimation {
|
|
||||||
Anim {
|
|
||||||
target: lockContent
|
|
||||||
property: "scale"
|
|
||||||
to: 1
|
|
||||||
type: Anim.FastSpatial
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockContent
|
|
||||||
property: "rotation"
|
|
||||||
to: 360
|
|
||||||
duration: Tokens.anim.durations.expressiveFastSpatial
|
|
||||||
easing: Tokens.anim.standardAccel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ParallelAnimation {
|
|
||||||
Anim {
|
|
||||||
target: lockIcon
|
|
||||||
property: "rotation"
|
|
||||||
to: 360
|
|
||||||
easing: Tokens.anim.standardDecel
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
target: lockIcon
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
target: content
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: content
|
|
||||||
property: "scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockBg
|
|
||||||
property: "radius"
|
|
||||||
to: lockContent.Tokens.rounding.extraLarge * 1.5
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockContent
|
|
||||||
property: "implicitWidth"
|
|
||||||
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio
|
|
||||||
}
|
|
||||||
Anim {
|
|
||||||
target: lockContent
|
|
||||||
property: "implicitHeight"
|
|
||||||
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScreencopyView {
|
ScreencopyView {
|
||||||
id: background
|
id: background
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
captureSource: root.screen
|
captureSource: root.screen
|
||||||
opacity: 0
|
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
|
|
@ -168,57 +52,265 @@ WlSessionLockSurface {
|
||||||
blur: 1
|
blur: 1
|
||||||
blurMax: 64
|
blurMax: 64
|
||||||
blurMultiplier: 1
|
blurMultiplier: 1
|
||||||
|
brightness: -0.38
|
||||||
|
saturation: 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyboard capture (feed PAM; Escape does nothing) ───────────────────
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
Component.onCompleted: forceActiveFocus()
|
||||||
|
Keys.onPressed: event => root.pam.handleKey(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Top status row ─────────────────────────────────────────────────────
|
||||||
|
RowLayout {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: 22
|
||||||
|
anchors.leftMargin: 28
|
||||||
|
anchors.rightMargin: 28
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 7
|
||||||
|
NsIcon {
|
||||||
|
icon: "lock"
|
||||||
|
color: Theme.textMuted
|
||||||
|
size: 15
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: "Locked"
|
||||||
|
color: Theme.textMuted
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: Theme.font.bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: lockContent
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4
|
RowLayout {
|
||||||
readonly property int radius: size / 4 * Tokens.rounding.scale
|
spacing: 14
|
||||||
|
|
||||||
|
NsIcon {
|
||||||
|
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
|
||||||
|
color: Theme.textMuted
|
||||||
|
size: 15
|
||||||
|
}
|
||||||
|
NsIcon {
|
||||||
|
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth_connected" : "bluetooth_disabled"
|
||||||
|
color: Theme.textMuted
|
||||||
|
size: 15
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
spacing: 6
|
||||||
|
visible: UPower.displayDevice.isLaptopBattery
|
||||||
|
NsIcon {
|
||||||
|
icon: Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged].includes(UPower.displayDevice.state))
|
||||||
|
color: Theme.textMuted
|
||||||
|
size: 15
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
|
||||||
|
color: Theme.textMuted
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: Theme.font.bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Centre block ───────────────────────────────────────────────────────
|
||||||
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: size
|
spacing: 0
|
||||||
implicitHeight: size
|
|
||||||
|
|
||||||
rotation: 180
|
StyledText {
|
||||||
scale: 0
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Time.format("hh:mm")
|
||||||
|
color: Theme.text
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: 108
|
||||||
|
font.weight: Font.ExtraLight
|
||||||
|
font.letterSpacing: 3
|
||||||
|
}
|
||||||
|
|
||||||
StyledRect {
|
StyledText {
|
||||||
id: lockBg
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Time.format("ddd, d MMMM yyyy")
|
||||||
|
color: Theme.textMuted
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: 64
|
||||||
|
implicitWidth: 74
|
||||||
|
implicitHeight: 74
|
||||||
|
radius: 37
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Vertical
|
||||||
|
GradientStop {
|
||||||
|
position: 0
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
position: 1
|
||||||
|
color: Theme.blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NsIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
icon: "person"
|
||||||
|
color: Theme.onAccent
|
||||||
|
size: 34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: 14
|
||||||
|
text: SysInfo.user || "user"
|
||||||
|
color: Theme.text
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: 16
|
||||||
|
implicitWidth: 312
|
||||||
|
implicitHeight: 44
|
||||||
|
radius: Theme.radius.panel
|
||||||
|
color: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.55)
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.errored ? Qt.rgba(224 / 255, 116 / 255, 106 / 255, 0.6) : Qt.rgba(1, 1, 1, 0.1)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Colours.palette.m3surface
|
anchors.leftMargin: 14
|
||||||
radius: parent.radius
|
anchors.rightMargin: 12
|
||||||
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
|
spacing: 10
|
||||||
|
|
||||||
layer.enabled: true
|
NsIcon {
|
||||||
layer.effect: MultiEffect {
|
icon: "key"
|
||||||
shadowEnabled: true
|
color: Theme.textMuted
|
||||||
blurMax: 15
|
size: 15
|
||||||
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.pam.buffer.length > 0 ? "•".repeat(root.pam.buffer.length) : "Enter password"
|
||||||
|
color: root.pam.buffer.length > 0 ? Theme.text : Theme.textFaint
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: Theme.font.body
|
||||||
|
font.letterSpacing: root.pam.buffer.length > 0 ? 2 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
NsIcon {
|
||||||
|
icon: "login"
|
||||||
|
color: root.pam.buffer.length > 0 ? Theme.accent : Theme.textFaint
|
||||||
|
size: 16
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -6
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: if (root.pam.buffer.length > 0)
|
||||||
|
root.pam.passwd.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
StyledText {
|
||||||
id: lockIcon
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.preferredHeight: 14
|
||||||
|
text: root.errored ? "Incorrect — try again" : (root.pam.lockMessage || "")
|
||||||
|
color: Theme.danger
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bottom strip ───────────────────────────────────────────────────────
|
||||||
|
RowLayout {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottomMargin: 26
|
||||||
|
anchors.leftMargin: 28
|
||||||
|
anchors.rightMargin: 28
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 10
|
||||||
|
visible: Players.active !== null
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitWidth: 42
|
||||||
|
implicitHeight: 42
|
||||||
|
radius: Theme.radius.panel
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Vertical
|
||||||
|
GradientStop {
|
||||||
|
position: 0
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
position: 1
|
||||||
|
color: Theme.blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NsIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "lock"
|
icon: "music_note"
|
||||||
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build()
|
color: Theme.onAccent
|
||||||
rotation: 180
|
size: 18
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Content {
|
ColumnLayout {
|
||||||
id: content
|
spacing: 0
|
||||||
|
StyledText {
|
||||||
|
text: Players.active?.trackTitle || ""
|
||||||
|
color: Theme.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.maximumWidth: 280
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: Theme.font.bodySmall
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: Players.active?.trackArtist || ""
|
||||||
|
color: Theme.textMuted
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.maximumWidth: 280
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: Theme.font.meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.centerIn: parent
|
Item {
|
||||||
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased
|
Layout.fillWidth: true
|
||||||
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased
|
}
|
||||||
|
|
||||||
lock: root
|
StyledText {
|
||||||
opacity: 0
|
text: "Press Enter to unlock"
|
||||||
scale: 0
|
color: Theme.textFaint
|
||||||
|
font.family: Theme.font.family
|
||||||
|
font.pixelSize: 11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,10 @@ Singleton {
|
||||||
"update": root.cp(0xf021),
|
"update": root.cp(0xf021),
|
||||||
"wallpaper": root.cp(0xf03e),
|
"wallpaper": root.cp(0xf03e),
|
||||||
"workspaces": root.cp(0xf009),
|
"workspaces": root.cp(0xf009),
|
||||||
"dock": root.cp(0xf2d1)
|
"dock": root.cp(0xf2d1),
|
||||||
|
// lock screen
|
||||||
|
"login": root.cp(0xf090),
|
||||||
|
"key": root.cp(0xf084)
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(name: string): string {
|
function get(name: string): string {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue