diff --git a/Modules/Lockscreen/Lockscreen.qml b/Modules/Lockscreen/Lockscreen.qml new file mode 100644 index 0000000..57e6933 --- /dev/null +++ b/Modules/Lockscreen/Lockscreen.qml @@ -0,0 +1,780 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Pam +import Quickshell.Io +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +WlSessionLock { + id: lock + + property string errorMessage: "" + property bool authenticating: false + property string password: "" + property bool pamAvailable: typeof PamContext !== "undefined" + locked: false + + 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 + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } + + // Blurred background + Rectangle { + anchors.fill: parent + 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) } + } + + // Subtle animated particles + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height + + SequentialAnimation on opacity { + loops: Animation.Infinite + 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 + anchors.right: parent.right + anchors.topMargin: 80 * Scaling.scale(screen) + spacing: 40 * Scaling.scale(screen) + + // Time display - Large and prominent with pulse animation + Column { + spacing: 8 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + Text { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: "Inter" + font.pixelSize: 140 * Scaling.scale(screen) + font.weight: Font.Bold + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + + 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 } + } + } + + Text { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: "Inter" + font.pixelSize: 26 * Scaling.scale(screen) + font.weight: Font.Light + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + // User section with animated avatar + Column { + spacing: 16 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + // Animated avatar with glow effect + Rectangle { + width: 120 * Scaling.scale(screen) + height: 120 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Colors.accentPrimary + border.width: 3 * Scaling.scale(screen) + anchors.horizontalCenter: parent.horizontalCenter + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * Scaling.scale(screen) + height: parent.height + 24 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + z: -1 + + 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 } + } + } + + NImageRounded { + anchors.centerIn: parent + width: 100 * Scaling.scale(screen) + height: 100 * Scaling.scale(screen) + imagePath: Quickshell.env("HOME") + "/.face" + fallbackIcon: "person" + imageRadius: width * 0.5 + } + + // Hover animation + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutBack } + } + } + + + } + } + + // Centered terminal section + Item { + width: 520 * Scaling.scale(screen) + height: 200 * Scaling.scale(screen) + anchors.centerIn: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 20 * Scaling.scale(screen) + width: parent.width + + + + // Futuristic Terminal-Style Input + Item { + width: parent.width + height: 200 * Scaling.scale(screen) + Layout.fillWidth: true + + // Terminal background with scanlines + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: 16 + color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Scanline effect + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Colors.applyOpacity(Colors.accentPrimary, "1A") + y: index * 10 + opacity: 0.3 + + SequentialAnimation on opacity { + loops: Animation.Infinite + 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.scale(screen) + color: Colors.applyOpacity(Colors.accentPrimary, "33") + topLeftRadius: 14 + topRightRadius: 14 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "●" + color: Colors.error + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.warning + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.accentPrimary + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "SECURE TERMINAL" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + font.weight: Font.Bold + Layout.fillWidth: true + } + } + } + + // Terminal content area + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: 50 * Scaling.scale(screen) + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + // Welcome back typing effect + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + id: welcomeText + text: "" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + property int currentIndex: 0 + property string fullText: "echo 'Welcome back, " + Quickshell.env("USER") + "!'" + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + // Command line with integrated password input + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + text: "sudo unlock_session" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + } + + // Integrated password input (invisible, just for functionality) + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lock.password + onTextChanged: { + lock.password = text + // Terminal typing sound effect (visual) + typingEffect.start() + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lock.unlockAttempt(); + } + } + + Component.onCompleted: { + forceActiveFocus(); + } + } + + // Visual password display with integrated cursor + Text { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + 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 + } + } + } + + // Blinking cursor positioned right after the asterisks + Rectangle { + width: 8 * Scaling.scale(screen) + height: 20 * Scaling.scale(screen) + color: Colors.accentPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: 2 * Scaling.scale(screen) + anchors.verticalCenter: asterisksText.verticalCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 500 } + NumberAnimation { to: 0.0; duration: 500 } + } + } + } + + // Status messages + Text { + text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") + color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lock.authenticating + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800 } + NumberAnimation { to: 0.5; duration: 800 } + } + } + + // Execute button + Rectangle { + width: 120 * Scaling.scale(screen) + height: 40 * Scaling.scale(screen) + radius: 12 + color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity(Colors.accentPrimary, "33") + border.color: Colors.accentPrimary + border.width: 1 + enabled: !lock.authenticating + Layout.alignment: Qt.AlignRight + + Text { + anchors.centerIn: parent + text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 12 * Scaling.scale(screen) + font.weight: Font.Bold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: lock.unlockAttempt() + + SequentialAnimation on scale { + running: containsMouse + NumberAnimation { to: 1.05; duration: 150; easing.type: Easing.OutCubic } + } + + SequentialAnimation on scale { + running: !containsMouse + NumberAnimation { to: 1.0; duration: 150; 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 } + } + } + } + + // Terminal glow effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") + border.width: 1 + z: -1 + + 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 } + } + } + } + + } + + // Error message with modern styling + Rectangle { + width: parent.width + height: 56 * Scaling.scale(screen) + radius: 28 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.15) + border.color: Colors.error + border.width: 1 * Scaling.scale(screen) + visible: lock.errorMessage !== "" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + anchors.margins: 18 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "error" + font.family: "Material Symbols Outlined" + font.pixelSize: 22 * Scaling.scale(screen) + color: Colors.error + } + + Text { + text: lock.errorMessage + color: Colors.error + font.family: "Inter" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Medium + Layout.fillWidth: true + } + } + + NumberAnimation on opacity { + from: 0 + to: 1 + duration: 300 + running: lock.errorMessage !== "" + } + + // Shake animation on error + SequentialAnimation on x { + running: lock.errorMessage !== "" + NumberAnimation { to: 10; duration: 50 } + NumberAnimation { to: -10; duration: 100 } + NumberAnimation { to: 10; duration: 100 } + NumberAnimation { to: 0; duration: 50 } + } + } + + + } + } + } + + // Enhanced power buttons with hover effects + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * Scaling.scale(screen) + spacing: 20 * Scaling.scale(screen) + + // Shutdown with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.error + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: shutdownArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: shutdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + 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: 28 * Scaling.scale(screen) + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: shutdownArea.containsMouse ? 1.1 : 1.0 + } + + // Reboot with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: rebootArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: rebootArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + 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: 28 * Scaling.scale(screen) + color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: rebootArea.containsMouse ? 1.1 : 1.0 + } + + // Logout with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentSecondary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: logoutArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: logoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + 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: 28 * Scaling.scale(screen) + color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: logoutArea.containsMouse ? 1.1 : 1.0 + } + } + + // Timer for updating time + 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"); + } + } + } +} diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 23f19df..33d7a4b 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -14,6 +14,8 @@ NBox { readonly property real scaling: Scaling.scale(screen) property string uptimeText: "--" + + Layout.fillWidth: true // Height driven by content diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 5a8a6c6..a0a7f9a 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -6,10 +6,13 @@ import Quickshell.Io import Quickshell.Widgets import qs.Services import qs.Widgets +import qs.Modules.Lockscreen NPanel { id: powerMenu visible: false + + // Anchors will be set by the parent component function show() { @@ -110,8 +113,9 @@ NPanel { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - // TODO: Implement lock screen functionality console.log("Lock screen requested") + // Lock the screen + lockScreen.locked = true powerMenu.visible = false } } @@ -416,4 +420,9 @@ NPanel { command: ["loginctl", "terminate-user", Quickshell.env("USER")] running: false } + + // Lockscreen instance + Lockscreen { + id: lockScreen + } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 96de4ce..ba3b546 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -15,6 +15,7 @@ NLoader { // Target screen to open on property var targetScreen: null + function openAt(x, screen) { anchorX = x targetScreen = screen diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index f9f8d8f..1a6f201 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -1,8 +1,12 @@ import QtQuick import Quickshell.Io +import qs.Modules.Lockscreen Item { id: root + + // Reference to the lockscreen component + property var lockscreen: null IpcHandler { target: "settings" @@ -40,7 +44,13 @@ Item { IpcHandler { target: "lockScreen" - function toggle() {// TODO + function toggle() { + lockScreen.locked = !lockScreen.locked } } + + // Lockscreen instance + Lockscreen { + id: lockScreen + } }