From 2a652d549a56b74cdf5134ffee7d84ef5e96823f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:50:39 +1000 Subject: feat: lock screen --- modules/lock/Backgrounds.qml | 216 ++++++++++++++++++++++++++++++++++++ modules/lock/Clock.qml | 56 ++++++++++ modules/lock/Input.qml | 254 +++++++++++++++++++++++++++++++++++++++++++ modules/lock/Lock.qml | 36 ++++++ modules/lock/LockSurface.qml | 97 +++++++++++++++++ 5 files changed, 659 insertions(+) create mode 100644 modules/lock/Backgrounds.qml create mode 100644 modules/lock/Clock.qml create mode 100644 modules/lock/Input.qml create mode 100644 modules/lock/Lock.qml create mode 100644 modules/lock/LockSurface.qml (limited to 'modules') diff --git a/modules/lock/Backgrounds.qml b/modules/lock/Backgrounds.qml new file mode 100644 index 0000000..78a4799 --- /dev/null +++ b/modules/lock/Backgrounds.qml @@ -0,0 +1,216 @@ +import "root:/widgets" +import "root:/services" +import "root:/config" +import QtQuick +import QtQuick.Shapes +import QtQuick.Effects + +Item { + id: root + + required property bool locked + + readonly property real clockBottom: innerMask.anchors.margins + clockPath.height + readonly property real inputTop: innerMask.anchors.margins + inputPath.height + + anchors.fill: parent + + StyledRect { + id: base + + anchors.fill: parent + color: Colours.alpha(Config.border.colour, false) + visible: false + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + id: innerMask + + anchors.fill: parent + anchors.margins: root.locked ? root.height * Config.lock.sizes.border : 0 + anchors.leftMargin: root.locked ? root.width * Config.lock.sizes.border : 0 + anchors.rightMargin: root.locked ? root.width * Config.lock.sizes.border : 0 + + radius: Appearance.rounding.large * 2 + + Behavior on anchors.margins { + Anim {} + } + + Behavior on anchors.leftMargin { + Anim {} + } + + Behavior on anchors.rightMargin { + Anim {} + } + } + } + + MultiEffect { + anchors.fill: parent + source: base + maskEnabled: true + maskInverted: true + maskSource: mask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Shape { + anchors.fill: parent + anchors.margins: Math.floor(innerMask.anchors.margins) + anchors.leftMargin: innerMask.anchors.leftMargin + anchors.rightMargin: innerMask.anchors.rightMargin + + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: clockPath + + readonly property int width: Config.lock.sizes.clockWidth + property real height: root.locked ? Config.lock.sizes.clockHeight : 0 + + readonly property real rounding: Appearance.rounding.large * 4 + readonly property bool flatten: height < rounding * 2 + readonly property real roundingY: flatten ? height / 2 : rounding + + strokeWidth: -1 + fillColor: Config.border.colour + + startX: (innerMask.width - width) / 2 - rounding + + PathArc { + relativeX: clockPath.rounding + relativeY: clockPath.roundingY + radiusX: clockPath.rounding + radiusY: Math.min(clockPath.rounding, clockPath.height) + } + PathLine { + relativeX: 0 + relativeY: clockPath.height - clockPath.roundingY * 2 + } + PathArc { + relativeX: clockPath.rounding + relativeY: clockPath.roundingY + radiusX: clockPath.rounding + radiusY: Math.min(clockPath.rounding, clockPath.height) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: clockPath.width - clockPath.rounding * 2 + relativeY: 0 + } + PathArc { + relativeX: clockPath.rounding + relativeY: -clockPath.roundingY + radiusX: clockPath.rounding + radiusY: Math.min(clockPath.rounding, clockPath.height) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: 0 + relativeY: -(clockPath.height - clockPath.roundingY * 2) + } + PathArc { + relativeX: clockPath.rounding + relativeY: -clockPath.roundingY + radiusX: clockPath.rounding + radiusY: Math.min(clockPath.rounding, clockPath.height) + } + + Behavior on height { + Anim {} + } + + Behavior on fillColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + ShapePath { + id: inputPath + + readonly property int width: Config.lock.sizes.inputWidth + property real height: root.locked ? Config.lock.sizes.inputHeight : 0 + + readonly property real rounding: Appearance.rounding.large * 2 + readonly property bool flatten: height < rounding * 2 + readonly property real roundingY: flatten ? height / 2 : rounding + + strokeWidth: -1 + fillColor: Config.border.colour + + startX: (innerMask.width - width) / 2 - rounding + startY: Math.ceil(innerMask.height) + + PathArc { + relativeX: inputPath.rounding + relativeY: -inputPath.roundingY + radiusX: inputPath.rounding + radiusY: Math.min(inputPath.rounding, inputPath.height) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: 0 + relativeY: -(inputPath.height - inputPath.roundingY * 2) + } + PathArc { + relativeX: inputPath.rounding + relativeY: -inputPath.roundingY + radiusX: inputPath.rounding + radiusY: Math.min(inputPath.rounding, inputPath.height) + } + PathLine { + relativeX: inputPath.width - inputPath.rounding * 2 + relativeY: 0 + } + PathArc { + relativeX: inputPath.rounding + relativeY: inputPath.roundingY + radiusX: inputPath.rounding + radiusY: Math.min(inputPath.rounding, inputPath.height) + } + PathLine { + relativeX: 0 + relativeY: inputPath.height - inputPath.roundingY * 2 + } + PathArc { + relativeX: inputPath.rounding + relativeY: inputPath.roundingY + radiusX: inputPath.rounding + radiusY: Math.min(inputPath.rounding, inputPath.height) + direction: PathArc.Counterclockwise + } + + Behavior on height { + Anim {} + } + + Behavior on fillColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.large + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.emphasized + } +} diff --git a/modules/lock/Clock.qml b/modules/lock/Clock.qml new file mode 100644 index 0000000..3d4da9e --- /dev/null +++ b/modules/lock/Clock.qml @@ -0,0 +1,56 @@ +import "root:/widgets" +import "root:/services" +import "root:/config" +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property bool locked + + spacing: 0 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignVCenter + text: Time.format("HH") + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge * 4 + font.family: Appearance.font.family.mono + font.weight: 800 + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + text: ":" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.extraLarge * 4 + font.family: Appearance.font.family.mono + font.weight: 800 + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + text: Time.format("mm") + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge * 4 + font.family: Appearance.font.family.mono + font.weight: 800 + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: Appearance.padding.large * 3 + + text: Time.format("dddd, d MMMM yyyy") + color: Colours.palette.m3tertiary + font.pointSize: Appearance.font.size.extraLarge + font.family: Appearance.font.family.mono + font.bold: true + } +} diff --git a/modules/lock/Input.qml b/modules/lock/Input.qml new file mode 100644 index 0000000..922dce8 --- /dev/null +++ b/modules/lock/Input.qml @@ -0,0 +1,254 @@ +import "root:/widgets" +import "root:/services" +import "root:/config" +import "root:/utils" +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Pam +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property WlSessionLockSurface lock + + property string passwordBuffer + + spacing: Appearance.spacing.large * 2 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.padding.large * 3 + Layout.maximumWidth: Config.lock.sizes.inputWidth - Appearance.rounding.large * 2 + + spacing: Appearance.spacing.large + + StyledClippingRect { + Layout.alignment: Qt.AlignVCenter + implicitWidth: Config.lock.sizes.faceSize + implicitHeight: Config.lock.sizes.faceSize + + radius: Appearance.rounding.large + color: Colours.palette.m3surfaceContainer + + MaterialIcon { + anchors.centerIn: parent + + text: "person" + fill: 1 + font.pointSize: Config.lock.sizes.faceSize / 2 + } + + CachingImage { + anchors.fill: parent + path: `${Paths.home}/.face` + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Appearance.spacing.small + + StyledText { + Layout.fillWidth: true + text: qsTr("Welcome back, %1").arg(Quickshell.env("USER")) + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Logging in to %1").arg(Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")) + color: Colours.palette.m3tertiary + font.pointSize: Appearance.font.size.large + elide: Text.ElideRight + } + } + } + + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: charList.implicitWidth + Appearance.padding.large * 2 + Layout.preferredHeight: Appearance.font.size.normal + Appearance.padding.large * 2 + + focus: true + color: Colours.palette.m3surfaceContainer + radius: Appearance.rounding.small + clip: true + + Keys.onPressed: event => { + if (pam.active) + return; + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + placeholder.animate = false; + pam.start(); + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + charList.implicitWidth = charList.implicitWidth; // Break binding + root.passwordBuffer = ""; + } else { + root.passwordBuffer = root.passwordBuffer.slice(0, -1); + } + } else if ("abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { + charList.bindImWidth(); + root.passwordBuffer += event.text; + } + } + + PamContext { + id: pam + + onResponseRequiredChanged: { + if (!responseRequired) + return; + + respond(root.passwordBuffer); + charList.implicitWidth = charList.implicitWidth; // Break binding + root.passwordBuffer = ""; + placeholder.animate = true; + } + + onCompleted: res => { + if (res === PamResult.Success) + return root.lock.unlock(); + + if (res === PamResult.Error) + placeholder.pamState = "error"; + else if (res === PamResult.MaxTries) + placeholder.pamState = "max"; + else if (res === PamResult.Failed) + placeholder.pamState = "fail"; + + placeholderDelay.restart(); + } + } + + Timer { + id: placeholderDelay + + interval: 3000 + onTriggered: placeholder.pamState = "" + } + + StyledText { + id: placeholder + + property string pamState + + anchors.centerIn: parent + + text: { + if (pam.active) + return qsTr("Loading..."); + if (pamState === "error") + return qsTr("An error occured"); + if (pamState === "max") + return qsTr("You have reached the maximum number of tries"); + if (pamState === "fail") + return qsTr("Incorrect password"); + return qsTr("Enter your password"); + } + + animate: true + color: pam.active ? Colours.palette.m3secondary : pamState ? Colours.palette.m3error : Colours.palette.m3outline + font.pointSize: Appearance.font.size.larger + + opacity: root.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + function bindImWidth(): void { + imWidthBehavior.enabled = false; + implicitWidth = Qt.binding(() => Math.min(count * (Appearance.font.size.normal + spacing) - spacing, Config.lock.sizes.inputWidth - Appearance.rounding.large * 2 - Appearance.padding.large * 5)); + imWidthBehavior.enabled = true; + } + + anchors.centerIn: parent + + implicitWidth: Math.min(count * (Appearance.font.size.normal + spacing) - spacing, Config.lock.sizes.inputWidth - Appearance.rounding.large * 2 - Appearance.padding.large * 5) + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + + model: ScriptModel { + values: root.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: Appearance.font.size.normal + implicitHeight: Appearance.font.size.normal + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.full + + opacity: 0 + scale: 0.5 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + } + + Behavior on implicitWidth { + id: imWidthBehavior + + Anim {} + } + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml new file mode 100644 index 0000000..1eca886 --- /dev/null +++ b/modules/lock/Lock.qml @@ -0,0 +1,36 @@ +pragma ComponentBehavior: Bound + +import "root:/widgets" +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +Scope { + WlSessionLock { + id: lock + + LockSurface { + lock: lock + } + } + + CustomShortcut { + name: "lock" + description: "Lock the current session" + onPressed: lock.locked = true + } + + CustomShortcut { + name: "unlock" + description: "Unlock the current session" + onPressed: lock.locked = false + } + + IpcHandler { + target: "lock" + + function lock(): void { + lock.locked = true; + } + } +} diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml new file mode 100644 index 0000000..1b98c2c --- /dev/null +++ b/modules/lock/LockSurface.qml @@ -0,0 +1,97 @@ +import "root:/widgets" +import "root:/services" +import "root:/config" +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects + +WlSessionLockSurface { + id: root + + required property WlSessionLock lock + + property bool locked + + function unlock(): void { + locked = false; + background.opacity = 0; + animDelay.start(); + } + + Component.onCompleted: locked = true + + color: "transparent" + + Timer { + id: animDelay + + interval: Appearance.anim.durations.large + onTriggered: root.lock.locked = false + } + + ScreencopyView { + id: screencopy + + anchors.fill: parent + captureSource: root.screen + visible: false + } + + MultiEffect { + id: background + + anchors.fill: parent + + source: screencopy + autoPaddingEnabled: false + blurEnabled: true + blur: root.locked ? 1 : 0 + blurMax: 64 + blurMultiplier: 1 + + Behavior on opacity { + Anim {} + } + + Behavior on blur { + Anim {} + } + } + + Backgrounds { + id: backgrounds + + locked: root.locked + visible: false + } + + MultiEffect { + anchors.fill: source + source: backgrounds + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) + } + + Clock { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.top + anchors.bottomMargin: -backgrounds.clockBottom + + locked: root.locked + } + + Input { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.bottom + anchors.topMargin: -backgrounds.inputTop + + lock: root + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.large + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} -- cgit v1.2.3-freya