Rework LockScreen auth logic

This commit is contained in:
Ly-sec 2025-08-24 19:38:20 +02:00
parent 4d8cf2207d
commit 76626dc8da
2 changed files with 770 additions and 1007 deletions

View file

@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pam
Scope {
id: root
signal unlocked()
signal failed()
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
property string errorMessage: ""
property bool pamAvailable: typeof PamContext !== "undefined"
onCurrentTextChanged: {
if (currentText !== "") {
showFailure = false;
errorMessage = "";
}
}
function tryUnlock() {
if (!pamAvailable) {
errorMessage = "PAM not available";
showFailure = true;
return;
}
if (currentText === "") {
errorMessage = "Password required";
showFailure = true;
return;
}
root.unlockInProgress = true;
errorMessage = "";
showFailure = false;
console.log("Starting PAM authentication for user:", pam.user);
pam.start();
}
PamContext {
id: pam
config: "login"
user: Quickshell.env("USER")
onPamMessage: {
console.log("PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired);
if (messageIsError) {
errorMessage = message;
}
if (responseRequired) {
console.log("Responding to PAM with password");
respond(root.currentText);
}
}
onResponseRequiredChanged: {
console.log("Response required changed:", responseRequired);
if (responseRequired && root.unlockInProgress) {
console.log("Automatically responding to PAM");
respond(root.currentText);
}
}
onCompleted: {
console.log("PAM completed with result:", result);
if (result === PamResult.Success) {
console.log("Authentication successful");
root.unlocked();
} else {
console.log("Authentication failed");
errorMessage = "Authentication failed";
showFailure = true;
root.failed();
}
root.unlockInProgress = false;
}
onError: {
console.log("PAM error:", error, "message:", message);
errorMessage = message || "Authentication error";
showFailure = true;
root.unlockInProgress = false;
root.failed();
}
}
}

View file

@ -17,163 +17,67 @@ Loader {
id: lockScreen
active: false
// Log state changes to help debug lock screen issues
onActiveChanged: {
Logger.log("LockScreen", "State changed:", active)
}
// Allow a small grace period after unlocking so the compositor releases the lock surfaces
Timer {
id: unloadAfterUnlockTimer
interval: 250
repeat: false
onTriggered: {
Logger.log("LockScreen", "Unload timer triggered - deactivating")
lockScreen.active = false
}
}
function scheduleUnloadAfterUnlock() {
Logger.log("LockScreen", "Scheduling unload after unlock")
unloadAfterUnlockTimer.start()
}
sourceComponent: Component {
WlSessionLock {
id: lock
// Tie session lock to loader visibility
sourceComponent: Component {
Item {
id: lockContainer
// Create the lock context
LockContext {
id: lockContext
onUnlocked: {
lockSession.locked = false
lockScreen.scheduleUnloadAfterUnlock()
lockContext.currentText = ""
}
}
WlSessionLock {
id: lockSession
locked: lockScreen.active
property string errorMessage: ""
property bool authenticating: false
property string password: ""
property bool pamAvailable: typeof PamContext !== "undefined"
function unlockAttempt() {
Logger.log("LockScreen", "Unlock attempt started")
// Real PAM authentication
if (!pamAvailable) {
lock.errorMessage = "PAM authentication not available."
Logger.log("LockScreen", "PAM not available")
return
}
if (!lock.password) {
lock.errorMessage = "Password required."
Logger.log("LockScreen", "No password entered")
return
}
Logger.log("LockScreen", "Starting PAM authentication")
lock.authenticating = true
lock.errorMessage = ""
Logger.log("LockScreen", "About to create PAM context with userName:", Quickshell.env("USER"))
var pam = Qt.createQmlObject(
'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }',
lock)
Logger.log("LockScreen", "PamContext created", pam)
pam.onCompleted.connect(function (result) {
Logger.log("LockScreen", "PAM completed with result:", result)
lock.authenticating = false
if (result === PamResult.Success) {
Logger.log("LockScreen", "Authentication successful, unlocking")
// First release the Wayland session lock, then unload after a short delay
lock.locked = false
lockScreen.scheduleUnloadAfterUnlock()
lock.password = ""
lock.errorMessage = ""
} else {
Logger.log("LockScreen", "Authentication failed")
lock.errorMessage = "Authentication failed."
lock.password = ""
}
pam.destroy()
})
pam.onError.connect(function (error) {
Logger.log("LockScreen", "PAM error:", error)
lock.authenticating = false
lock.errorMessage = pam.message || "Authentication error."
lock.password = ""
pam.destroy()
})
pam.onPamMessage.connect(function () {
Logger.log("LockScreen", "PAM message:", pam.message, "isError:", pam.messageIsError)
if (pam.messageIsError) {
lock.errorMessage = pam.message
}
})
pam.onResponseRequiredChanged.connect(function () {
Logger.log("LockScreen", "PAM response required:", pam.responseRequired)
if (pam.responseRequired && lock.authenticating) {
Logger.log("LockScreen", "Responding to PAM with password")
pam.respond(lock.password)
}
})
var started = pam.start()
Logger.log("LockScreen", "PAM start result:", started)
}
WlSessionLockSurface {
// Battery indicator component
// WlSessionLockSurface provides a screen variable for the current screen.
// Also we use a different scaling algorithm based on the resolution, as the design is full screen.
readonly property real scaling: ScalingService.dynamicScale(screen)
Item {
id: batteryIndicator
// Import UPower for battery data
property var battery: UPower.displayDevice
property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
property real percent: isReady ? (battery.percentage * 100) : 0
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
property bool batteryVisible: isReady && percent > 0
// Choose icon based on charge and charging state
function getIcon() {
if (!batteryVisible)
return ""
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
if (!batteryVisible) return ""
if (charging) return "battery_android_bolt"
if (percent >= 95) return "battery_android_full"
if (percent >= 85) return "battery_android_6"
if (percent >= 70) return "battery_android_5"
if (percent >= 55) return "battery_android_4"
if (percent >= 40) return "battery_android_3"
if (percent >= 25) return "battery_android_2"
if (percent >= 10) return "battery_android_1"
if (percent >= 0) return "battery_android_0"
}
}
// Keyboard layout indicator component
Item {
id: keyboardLayout
property string currentLayout: (typeof KeyboardLayoutService !== 'undefined'
&& KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown"
property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown"
}
// Wallpaper image
Image {
id: lockBgImage
anchors.fill: parent
@ -184,40 +88,23 @@ Loader {
mipmap: false
}
// Blurred background
Rectangle {
anchors.fill: parent
color: Color.transparent
// Simple blur effect
layer.enabled: true
layer.smooth: true
layer.samples: 4
}
// Animated gradient overlay
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop {
position: 0.0
color: Qt.rgba(0, 0, 0, 0.6)
}
GradientStop {
position: 0.3
color: Qt.rgba(0, 0, 0, 0.3)
}
GradientStop {
position: 0.7
color: Qt.rgba(0, 0, 0, 0.4)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 0, 0.7)
}
GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) }
GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) }
GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) }
GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) }
}
// Subtle animated particles
Repeater {
model: 20
Rectangle {
@ -230,24 +117,16 @@ Loader {
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.8
duration: 2000 + Math.random() * 3000
}
NumberAnimation {
to: 0.1
duration: 2000 + Math.random() * 3000
}
NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 }
NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 }
}
}
}
}
// Main content - Centered design
Item {
anchors.fill: parent
// Top section - Time, date, and user info
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
@ -255,7 +134,6 @@ Loader {
anchors.topMargin: 80 * scaling
spacing: 40 * scaling
// Time display - Large and prominent with pulse animation
Column {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter
@ -272,16 +150,8 @@ Loader {
SequentialAnimation on scale {
loops: Animation.Infinite
NumberAnimation {
to: 1.02
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad }
}
}
@ -297,12 +167,10 @@ Loader {
}
}
// User section with animated avatar
Column {
spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignHCenter
// Animated avatar with glow effect or audio visualizer
Rectangle {
width: 108 * scaling
height: 108 * scaling
@ -313,31 +181,25 @@ Loader {
anchors.horizontalCenter: parent.horizontalCenter
z: 10
// Circular audio visualizer when music is playing
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
sourceComponent: Item {
Repeater {
model: CavaService.values.length
Rectangle {
property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI
property real linearRadius: 70 * scaling
property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling)
property real linearBarWidth: 3 * scaling
width: linearBarWidth
height: linearBarLength
color: Color.mPrimary
radius: linearBarWidth * 0.5
x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5
y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5
transform: Rotation {
origin.x: linearBarWidth * 0.5
origin.y: linearBarLength * 0.5
@ -353,28 +215,21 @@ Loader {
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
sourceComponent: Item {
Repeater {
model: CavaService.values.length * 2
Rectangle {
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length
* 2 - 1 - index)
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length * 2 - 1 - index)
property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI
property real mirroredRadius: 70 * scaling
property real mirroredBarLength: Math.max(2,
CavaService.values[mirroredValueIndex] * 30 * scaling)
property real mirroredBarLength: Math.max(2, CavaService.values[mirroredValueIndex] * 30 * scaling)
property real mirroredBarWidth: 3 * scaling
width: mirroredBarWidth
height: mirroredBarLength
color: Color.mPrimary
radius: mirroredBarWidth * 0.5
x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5
y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5
transform: Rotation {
origin.x: mirroredBarWidth * 0.5
origin.y: mirroredBarLength * 0.5
@ -390,65 +245,46 @@ Loader {
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
sourceComponent: Item {
Canvas {
id: waveCanvas
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
if (CavaService.values.length === 0) {
return
}
if (CavaService.values.length === 0) return
ctx.strokeStyle = Color.mPrimary
ctx.lineWidth = 2 * scaling
ctx.lineCap = "round"
var centerX = width * 0.5
var centerY = height * 0.5
var baseRadius = 60 * scaling
var maxAmplitude = 20 * scaling
ctx.beginPath()
for (var i = 0; i <= CavaService.values.length; i++) {
var index = i % CavaService.values.length
var angle = (i / CavaService.values.length) * 2 * Math.PI
var amplitude = CavaService.values[index] * maxAmplitude
var radius = baseRadius + amplitude
var x = centerX + Math.cos(angle) * radius
var y = centerY + Math.sin(angle) * radius
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
}
ctx.closePath()
ctx.stroke()
}
}
Timer {
interval: 16 // ~60 FPS
interval: 16
running: true
repeat: true
onTriggered: {
waveCanvas.requestPaint()
}
onTriggered: waveCanvas.requestPaint()
}
}
}
// Glow effect when no music is playing
Rectangle {
anchors.centerIn: parent
width: parent.width + 24 * scaling
@ -459,19 +295,10 @@ Loader {
border.width: Math.max(1, Style.borderM * scaling)
z: -1
visible: !MediaService.isPlaying
SequentialAnimation on scale {
loops: Animation.Infinite
NumberAnimation {
to: 1.1
duration: 1500
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 1500
easing.type: Easing.InOutQuad
}
NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad }
}
}
@ -483,7 +310,6 @@ Loader {
fallbackIcon: "person"
}
// Hover animation
MouseArea {
anchors.fill: parent
hoverEnabled: true
@ -492,28 +318,23 @@ Loader {
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
NumberAnimation { duration: Style.animationFast; easing.type: Easing.OutBack }
}
}
}
}
// Centered terminal section
Item {
width: 720 * scaling
height: 280 * scaling
anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling
// Futuristic Terminal-Style Input
Item {
width: parent.width
height: 280 * scaling
Layout.fillWidth: true
// Terminal background with scanlines
Rectangle {
id: terminalBackground
anchors.fill: parent
@ -522,7 +343,6 @@ Loader {
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
// Scanline effect
Repeater {
model: 20
Rectangle {
@ -531,22 +351,14 @@ Loader {
color: Color.applyOpacity(Color.mPrimary, "1A")
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.1
duration: 2000 + Math.random() * 1000
}
NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 }
NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 }
}
}
}
// Terminal header
Rectangle {
width: parent.width
height: 40 * scaling
@ -571,17 +383,14 @@ Loader {
Layout.fillWidth: true
}
// Battery indicator
Row {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
@ -591,10 +400,8 @@ Loader {
}
}
// Keyboard layout indicator
Row {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
@ -602,7 +409,6 @@ Loader {
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
font.pointSize: Style.fontSizeM * scaling
@ -612,7 +418,6 @@ Loader {
}
}
// Terminal content area
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
@ -622,7 +427,6 @@ Loader {
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
// Welcome back typing effect
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
@ -660,7 +464,6 @@ Loader {
}
}
// Command line with integrated password input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
@ -680,7 +483,6 @@ Loader {
font.pointSize: Style.fontSizeL * scaling
}
// Integrated password input (invisible, just for functionality)
TextInput {
id: passwordInput
width: 0
@ -693,16 +495,14 @@ Loader {
passwordCharacter: "*"
passwordMaskDelay: 0
text: lock.password
text: lockContext.currentText
onTextChanged: {
lock.password = text
// Terminal typing sound effect (visual)
typingEffect.start()
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt()
lockContext.tryUnlock()
}
}
@ -711,7 +511,6 @@ Loader {
}
}
// Visual password display with integrated cursor
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
@ -720,25 +519,13 @@ Loader {
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
// Typing effect animation
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
NumberAnimation { target: passwordInput; property: "scale"; to: 1.01; duration: 50 }
NumberAnimation { target: passwordInput; property: "scale"; to: 1.0; duration: 50 }
}
}
// Blinking cursor positioned right after the asterisks
Rectangle {
width: 8 * scaling
height: 20 * scaling
@ -749,41 +536,36 @@ Loader {
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
NumberAnimation { to: 1.0; duration: 500 }
NumberAnimation { to: 0.0; duration: 500 }
}
}
}
// Status messages
NText {
text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "")
color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent)
text: {
if (lockContext.unlockInProgress) return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage) return lockContext.errorMessage
if (lockContext.showFailure) return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress) return Color.mPrimary
if (lockContext.showFailure) return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lock.authenticating
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
NumberAnimation { to: 1.0; duration: 800 }
NumberAnimation { to: 0.5; duration: 800 }
}
}
// Execute button
Row {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
@ -794,11 +576,11 @@ Loader {
color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33")
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lock.authenticating
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lock.authenticating ? "EXECUTING" : "EXECUTE"
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
@ -809,47 +591,31 @@ Loader {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: lock.unlockAttempt()
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
NumberAnimation { to: 1.05; duration: Style.animationFast; easing.type: Easing.OutCubic }
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
NumberAnimation { to: 1.0; duration: Style.animationFast; easing.type: Easing.OutCubic }
}
}
// Processing animation
SequentialAnimation on scale {
loops: Animation.Infinite
running: lock.authenticating
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
running: lockContext.unlockInProgress
NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
}
}
}
}
// Terminal glow effect
Rectangle {
anchors.fill: parent
radius: parent.radius
@ -860,194 +626,98 @@ Loader {
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.2
duration: 2000
easing.type: Easing.InOutQuad
}
}
NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad }
NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad }
}
}
}
}
}
// Enhanced power buttons with hover effects
// Power buttons at bottom
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 50 * scaling
spacing: 20 * scaling
// Shutdown with enhanced styling
Rectangle {
width: 64 * scaling
height: 64 * scaling
radius: Style.radiusL * scaling
color: shutdownArea.containsMouse ? Color.applyOpacity(Color.mError,
"DD") : Color.applyOpacity(Color.mError, "22")
width: 60 * scaling
height: 60 * scaling
radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Color.applyOpacity(Color.mError, "33")
border.color: Color.mError
border.width: Math.max(1, Style.borderM * scaling)
// Glow effect
Rectangle {
NIcon {
anchors.centerIn: parent
width: parent.width + 10 * scaling
height: parent.height + 10 * scaling
radius: width * 0.5
color: Color.transparent
opacity: shutdownArea.containsMouse ? 1 : 0
z: -1
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
text: "power_settings_new"
font.pointSize: Style.fontSizeXL * scaling
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
}
MouseArea {
id: shutdownArea
id: powerButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
CompositorService.shutdown()
}
}
NIcon {
text: "power_settings_new"
font.pointSize: Style.fontSizeXXXL * scaling
color: shutdownArea.containsMouse ? Color.mOnPrimary : Color.mError
anchors.centerIn: parent
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
scale: shutdownArea.containsMouse ? 1.1 : 1.0
}
// Reboot with enhanced styling
Rectangle {
width: 64 * scaling
height: 64 * scaling
radius: Style.radiusL * scaling
color: rebootArea.containsMouse ? Color.applyOpacity(Color.mPrimary,
"DD") : Color.applyOpacity(Color.mPrimary, "22")
width: 60 * scaling
height: 60 * scaling
radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33")
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
// Glow effect
Rectangle {
NIcon {
anchors.centerIn: parent
width: parent.width + 10 * scaling
height: parent.height + 10 * scaling
radius: width * 0.5
color: Color.transparent
opacity: rebootArea.containsMouse ? 1 : 0
z: -1
Behavior on opacity {
NumberAnimation {
duration: Style.animationMedium
easing.type: Easing.OutCubic
}
}
text: "restart_alt"
font.pointSize: Style.fontSizeXL * scaling
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
}
MouseArea {
id: rebootArea
id: restartButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
CompositorService.reboot()
}
}
NIcon {
text: "refresh"
font.pointSize: Style.fontSizeXXXL * scaling
color: rebootArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
anchors.centerIn: parent
}
Behavior on color {
ColorAnimation {
duration: Style.animationMedium
easing.type: Easing.OutCubic
}
}
scale: rebootArea.containsMouse ? 1.1 : 1.0
}
// Logout with enhanced styling
Rectangle {
width: 64 * scaling
height: 64 * scaling
radius: Style.radiusL * scaling
color: logoutArea.containsMouse ? Color.applyOpacity(Color.mSecondary,
"DD") : Color.applyOpacity(Color.mSecondary, "22")
width: 60 * scaling
height: 60 * scaling
radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Color.applyOpacity(Color.mSecondary, "33")
border.color: Color.mSecondary
border.width: Math.max(1, Style.borderM * scaling)
// Glow effect
Rectangle {
NIcon {
anchors.centerIn: parent
width: parent.width + 10 * scaling
height: parent.height + 10 * scaling
radius: width * 0.5
color: Color.transparent
opacity: logoutArea.containsMouse ? 1 : 0
z: -1
Behavior on opacity {
NumberAnimation {
duration: Style.animationMedium
easing.type: Easing.OutCubic
}
}
text: "bedtime"
font.pointSize: Style.fontSizeXL * scaling
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
}
MouseArea {
id: logoutArea
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
CompositorService.logout()
CompositorService.suspend()
}
}
}
}
}
NIcon {
text: "exit_to_app"
font.pointSize: Style.fontSizeXXXL * scaling
color: logoutArea.containsMouse ? Color.mOnPrimary : Color.mSecondary
anchors.centerIn: parent
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
scale: logoutArea.containsMouse ? 1.1 : 1.0
}
}
// Timer for updating time
Timer {
interval: 1000
running: true
@ -1061,3 +731,4 @@ Loader {
}
}
}
}