noctalia-shell/Widgets/LockScreen.qml
2025-07-11 14:14:28 +02:00

780 lines
No EOL
30 KiB
QML

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.Settings
import qs.Helpers
import "../Helpers/Weather.js" as WeatherHelper
// Password-only lockscreen for all screens
WlSessionLock {
id: lock
property bool demoMode: true // Set to true for demo/recording mode
property string errorMessage: ""
property bool authenticating: false
property string password: ""
property bool pamAvailable: typeof PamContext !== "undefined"
property string weatherCity: Settings.weatherCity
property var weatherData: null
property string weatherError: ""
property string weatherInfo: ""
property string weatherIcon: ""
property double currentTemp: 0
locked: false // Start unlocked, only lock when button is clicked
// On component completed, fetch weather data
Component.onCompleted: {
fetchWeatherData()
}
// Weather fetching function
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";
}
// Authentication function
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);
}
// Remove the surface property and use a Loader instead
Loader {
anchors.fill: parent
active: true
sourceComponent: demoMode ? demoComponent : lockComponent
}
Component {
id: demoComponent
Window {
id: demoWindow
visible: true
width: 900
height: 600
color: "transparent"
flags: Qt.Window | Qt.FramelessWindowHint
// Blurred wallpaper background
Image {
id: demoBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
cache: true
smooth: true
sourceSize.width: 2560
sourceSize.height: 1440
visible: true // Show the original for FastBlur input
}
FastBlur {
anchors.fill: parent
source: demoBgImage
radius: 48 // Adjust blur strength as needed
transparentBorder: true
}
// Main content container (moved up, Rectangle removed)
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
// User avatar/icon
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.profileImage
fillMode: Image.PreserveAspectCrop
visible: false // Only show the masked version
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.profileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.profileImage === ""
}
// Glow effect
layer.enabled: true
layer.effect: Glow {
color: Theme.accentPrimary
radius: 8
samples: 16
}
}
// Username
Text {
Layout.alignment: Qt.AlignHCenter
text: Settings.userName
font.pixelSize: 24
font.weight: Font.Medium
color: Theme.textPrimary
}
// Password input container
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
color: Theme.surface
opacity: 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.pixelSize: 16
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
text: lock.password
onTextChanged: lock.password = text
// Placeholder text
Text {
anchors.centerIn: parent
text: "Enter password..."
color: Theme.textSecondary
opacity: 0.6
font.pixelSize: 16
visible: !passwordInput.text && !passwordInput.activeFocus
}
// Handle Enter key
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt()
}
}
}
}
// Error message
Text {
Layout.alignment: Qt.AlignHCenter
text: lock.errorMessage
color: Theme.error
font.pixelSize: 14
visible: lock.errorMessage !== ""
opacity: lock.errorMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// Unlock button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: lock.authenticating ? "Authenticating..." : "Unlock"
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()
}
}
}
}
// Bypass Login button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: bypassButtonArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: "Bypass Login"
font.pixelSize: 16
font.bold: true
color: bypassButtonArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
MouseArea {
id: bypassButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.locked = false;
lock.errorMessage = "";
lock.password = "";
}
}
}
}
}
// Top-center info panel (clock + weather)
ColumnLayout {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 40
spacing: 8
// Clock
Text {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
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.pixelSize: 16
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
// Weather info (centered, no city)
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.useFahrenheit ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : (Settings.useFahrenheit ? "--°F" : "--°C")
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
// Weather error
Text {
text: weatherError
color: Theme.error
visible: weatherError !== ""
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
// Update clock every second
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")
}
}
// Update weather every 10 minutes
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
onTriggered: {
fetchWeatherData()
}
}
}
}
Component {
id: lockComponent
WlSessionLockSurface {
// Blurred wallpaper background
Image {
id: lockBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
cache: true
smooth: true
sourceSize.width: 2560
sourceSize.height: 1440
visible: true // Show the original for FastBlur input
}
FastBlur {
anchors.fill: parent
source: lockBgImage
radius: 48 // Adjust blur strength as needed
transparentBorder: true
}
// Main content container (moved up, Rectangle removed)
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
// User avatar/icon
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.profileImage
fillMode: Image.PreserveAspectCrop
visible: false // Only show the masked version
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.profileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.profileImage === ""
}
// Glow effect
layer.enabled: true
layer.effect: Glow {
color: Theme.accentPrimary
radius: 8
samples: 16
}
}
// Username
Text {
Layout.alignment: Qt.AlignHCenter
text: Quickshell.env("USER")
font.pixelSize: 24
font.weight: Font.Medium
color: Theme.textPrimary
}
// Password input container
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
color: Theme.surface
opacity: 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.pixelSize: 16
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
text: lock.password
onTextChanged: lock.password = text
// Placeholder text
Text {
anchors.centerIn: parent
text: "Enter password..."
color: Theme.textSecondary
opacity: 0.6
font.pixelSize: 16
visible: !passwordInput.text && !passwordInput.activeFocus
}
// Handle Enter key
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt()
}
}
}
}
// Error message
Text {
Layout.alignment: Qt.AlignHCenter
text: lock.errorMessage
color: Theme.error
font.pixelSize: 14
visible: lock.errorMessage !== ""
opacity: lock.errorMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// Unlock button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: lock.authenticating ? "Authenticating..." : "Unlock"
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()
}
}
}
}
// Bypass Login button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: bypassButtonArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: "Bypass Login"
font.pixelSize: 16
font.bold: true
color: bypassButtonArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
MouseArea {
id: bypassButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.locked = false;
lock.errorMessage = "";
lock.password = "";
}
}
}
}
}
// Top-center info panel (clock + weather)
ColumnLayout {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 40
spacing: 8
// Clock
Text {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
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.pixelSize: 16
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
// Weather info (centered, no city)
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.useFahrenheit ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : (Settings.useFahrenheit ? "--°F" : "--°C")
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
// Weather error
Text {
text: weatherError
color: Theme.error
visible: weatherError !== ""
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
// Update clock every second
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")
}
}
// Update weather every 10 minutes
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
onTriggered: {
fetchWeatherData()
}
}
// System control buttons (bottom right)
ColumnLayout {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 32
spacing: 12
// Shutdown
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
}
}
// Reboot
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
}
}
// Logout
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
}
}
}
}
}
}