LockScreen: Added a battery indicator

- relocated lockscreen to its own directory
This commit is contained in:
Sébastien Atoch 2025-08-02 08:29:33 -04:00
parent 049dd5d771
commit b1a6804fea
3 changed files with 95 additions and 5 deletions

View file

@ -0,0 +1,75 @@
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import QtQuick.Layouts
import qs.Components
import qs.Settings
Item {
// Test mode
property bool testMode: false
property int testPercent: 49
property bool testCharging: true
property var battery: UPower.displayDevice
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
property bool show: isReady && percent > 0
width: row.width
height: row.height
visible: testMode || (isReady && battery.isLaptopBattery)
// Choose icon based on charge and charging state
function batteryIcon() {
if (!show)
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";
}
RowLayout {
id: row
spacing: 6
Layout.alignment: Qt.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
Text {
text: batteryIcon()
font.family: "Material Symbols Outlined"
font.pixelSize: 28
color: charging ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVBottom
}
Text {
text: Math.round(percent) + "%"
font.family: Theme.fontFamily
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVBottom
}
}
}

View file

@ -0,0 +1,526 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell.Wayland
import Quickshell
import Quickshell.Services.Pam
import Quickshell.Io
import qs.Components
import qs.Settings
import qs.Services
import qs.Widgets.LockScreen
import "../../Helpers/Weather.js" as WeatherHelper
WlSessionLock {
id: lock
property string errorMessage: ""
property bool authenticating: false
property string password: ""
property bool pamAvailable: typeof PamContext !== "undefined"
property string weatherCity: Settings.settings.weatherCity
property var weatherData: null
property string weatherError: ""
property string weatherInfo: ""
property string weatherIcon: ""
property double currentTemp: 0
locked: false
// Request to fetch weather with a little delay to ensure weatherCity is properly loaded.
Component.onCompleted: {
Qt.callLater(function () {
fetchWeatherData();
})
}
function fetchWeatherData() {
WeatherHelper.fetchCityWeather(weatherCity, function (result) {
weatherData = result.weather;
weatherError = "";
}, function (err) {
weatherError = err;
});
}
function materialSymbolForCode(code) {
if (code === 0)
return "sunny";
if (code === 1 || code === 2)
return "partly_cloudy_day";
if (code === 3)
return "cloud";
if (code >= 45 && code <= 48)
return "foggy";
if (code >= 51 && code <= 67)
return "rainy";
if (code >= 71 && code <= 77)
return "weather_snowy";
if (code >= 80 && code <= 82)
return "rainy";
if (code >= 95 && code <= 99)
return "thunderstorm";
return "cloud";
}
function unlockAttempt() {
console.log("Unlock attempt started");
if (!pamAvailable) {
lock.errorMessage = "PAM authentication not available.";
console.log("PAM not available");
return;
}
if (!lock.password) {
lock.errorMessage = "Password required.";
console.log("No password entered");
return;
}
console.log("Starting PAM authentication...");
lock.authenticating = true;
lock.errorMessage = "";
console.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);
console.log("PamContext created", pam);
pam.onCompleted.connect(function (result) {
console.log("PAM completed with result:", result);
lock.authenticating = false;
if (result === PamResult.Success) {
console.log("Authentication successful, unlocking...");
lock.locked = false;
lock.password = "";
lock.errorMessage = "";
} else {
console.log("Authentication failed");
lock.errorMessage = "Authentication failed.";
lock.password = "";
}
pam.destroy();
});
pam.onError.connect(function (error) {
console.log("PAM error:", error);
lock.authenticating = false;
lock.errorMessage = pam.message || "Authentication error.";
lock.password = "";
pam.destroy();
});
pam.onPamMessage.connect(function () {
console.log("PAM message:", pam.message, "isError:", pam.messageIsError);
if (pam.messageIsError) {
lock.errorMessage = pam.message;
}
});
pam.onResponseRequiredChanged.connect(function () {
console.log("PAM response required:", pam.responseRequired);
if (pam.responseRequired && lock.authenticating) {
console.log("Responding to PAM with password");
pam.respond(lock.password);
}
});
var started = pam.start();
console.log("PAM start result:", started);
}
WlSessionLockSurface {
// Wallpaper image to blur
Image {
id: lockBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : ""
cache: true
smooth: false
visible: true // source for MultiEffect
}
MultiEffect {
id: lockBgBlur
anchors.fill: parent
source: lockBgImage
blurEnabled: true
blur: 0.48 // controls blur strength (0 to 1)
blurMax: 128 // max blur radius in pixels
}
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 80
height: 80
radius: 40
color: Theme.accentPrimary
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 4
source: Settings.settings.profileImage
fillMode: Image.PreserveAspectCrop
visible: false
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.settings.profileImage !== ""
}
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.settings.profileImage === ""
}
layer.enabled: true
layer.effect: Glow {
color: Theme.accentPrimary
radius: 8
samples: 16
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: Quickshell.env("USER")
font.family: Theme.fontFamily
font.pixelSize: 24
font.weight: Font.Medium
color: Theme.textPrimary
}
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
color: Theme.surface
opacity: passwordInput.activeFocus ? 0.8 : 0.3
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: 15
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
font.family: Theme.fontFamily
font.pixelSize: 16
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
text: lock.password
onTextChanged: lock.password = text
Text {
anchors.centerIn: parent
text: "Enter password..."
color: Theme.textSecondary
font.family: Theme.fontFamily
font.pixelSize: 16
visible: !passwordInput.text && !passwordInput.activeFocus
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt();
}
}
Component.onCompleted: {
forceActiveFocus();
}
}
}
Rectangle {
id: errorMessageRect
Layout.alignment: Qt.AlignHCenter
width: parent.width * 0.8
height: 44
color: Theme.overlay
radius: 22
visible: lock.errorMessage !== ""
Text {
anchors.centerIn: parent
text: lock.errorMessage
color: Theme.error
font.family: Theme.fontFamily
font.pixelSize: 14
opacity: 1
visible: lock.errorMessage !== ""
}
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
opacity: unlockButtonArea.containsMouse ? 0.8 : 0.5
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : Theme.surface
border.color: Theme.accentPrimary
border.width: 2
enabled: !lock.authenticating
Text {
id: unlockButtonText
anchors.centerIn: parent
text: lock.authenticating ? "..." : "Unlock"
font.family: Theme.fontFamily
font.pixelSize: 16
font.bold: true
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: unlockButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.unlockAttempt();
}
}
}
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
}
}
Corners {
id: topRightCorner
position: "bottomleft"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: screen.width / 2 + 38
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
z: 50
}
Corners {
id: topLeftCorner
position: "bottomright"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
offsetX: - Screen.width / 2 - 38
offsetY: 0
anchors.top: parent.top
visible: Settings.settings.showCorners
z: 51
}
Rectangle {
width: infoColumn.width + 16
height: infoColumn.height + 8
color: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
anchors.horizontalCenter: parent.horizontalCenter
bottomLeftRadius: 20
bottomRightRadius: 20
ColumnLayout {
id: infoColumn
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 0
anchors.bottomMargin: 0
spacing: 8
Text {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
font.family: Theme.fontFamily
font.pixelSize: 48
font.bold: true
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.family: Theme.fontFamily
font.pixelSize: 16
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
spacing: 6
Layout.alignment: Qt.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
visible: weatherData && weatherData.current_weather
Text {
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
color: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
}
Text {
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9 / 5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
font.family: Theme.fontFamily
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
Text {
text: weatherError
color: Theme.error
visible: weatherError !== ""
font.family: Theme.fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
timeText.text = Qt.formatDateTime(new Date(), "HH:mm");
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d");
}
}
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
onTriggered: {
fetchWeatherData();
}
}
ColumnLayout {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: 32
spacing: 12
BatteryCharge {
}
}
ColumnLayout {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 32
spacing: 12
Rectangle {
width: 48
height: 48
radius: 24
color: shutdownArea.containsMouse ? Theme.error : "transparent"
border.color: Theme.error
border.width: 1
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock);
}
}
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error
}
}
Rectangle {
width: 48
height: 48
radius: 24
color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock);
}
}
Text {
anchors.centerIn: parent
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
}
Rectangle {
width: 48
height: 48
radius: 24
color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 1
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock);
}
}
Text {
anchors.centerIn: parent
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
}
}
}
}