summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/lock/Center.qml162
-rw-r--r--modules/lock/Content.qml9
-rw-r--r--modules/lock/InputField.qml8
-rw-r--r--modules/lock/LockSurface.qml10
-rw-r--r--modules/lock/Media.qml2
-rw-r--r--modules/lock/NotifDock.qml6
-rw-r--r--modules/lock/Pam.qml127
7 files changed, 295 insertions, 29 deletions
diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml
index 91fcf2f..4db0376 100644
--- a/modules/lock/Center.qml
+++ b/modules/lock/Center.qml
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import qs.components
+import qs.components.controls
import qs.components.images
import qs.services
import qs.config
@@ -17,6 +18,7 @@ ColumnLayout {
readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale
Layout.preferredWidth: centerWidth
+ Layout.fillWidth: false
Layout.fillHeight: true
spacing: Appearance.spacing.large * 2
@@ -148,9 +150,34 @@ ColumnLayout {
anchors.margins: Appearance.padding.small
spacing: Appearance.spacing.normal
- MaterialIcon {
- Layout.leftMargin: Appearance.padding.smaller
- text: "lock"
+ Item {
+ implicitWidth: implicitHeight
+ implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2
+
+ MaterialIcon {
+ id: fprintIcon
+
+ anchors.centerIn: parent
+ animate: true
+ text: {
+ if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries)
+ return "fingerprint_off";
+ if (root.lock.pam.fprint.active)
+ return "fingerprint";
+ return "lock";
+ }
+ color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
+ opacity: root.lock.pam.passwd.active ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ StyledBusyIndicator {
+ anchors.fill: parent
+ running: root.lock.pam.passwd.active
+ }
}
InputField {
@@ -186,6 +213,135 @@ ColumnLayout {
}
}
+ StyledText {
+ id: message
+
+ readonly property Pam pam: root.lock.pam
+ readonly property string msg: {
+ if (pam.fprintState === "error")
+ return qsTr("ERROR: %1").arg(pam.fprint.message);
+ if (pam.state === "error")
+ return qsTr("ERROR: %1").arg(pam.passwd.message);
+
+ if (pam.lockMessage)
+ return pam.lockMessage;
+
+ if (pam.state === "max" && pam.fprintState === "max")
+ return qsTr("Maximum password and fingerprint attempts reached.");
+ if (pam.state === "max") {
+ if (pam.fprint.available)
+ return qsTr("Maximum password attempts reached. Please use fingerprint.");
+ return qsTr("Maximum password attempts reached.");
+ }
+ if (pam.fprintState === "max")
+ return qsTr("Maximum fingerprint attempts reached. Please use password.");
+
+ if (pam.state === "fail") {
+ if (pam.fprint.available)
+ return qsTr("Incorrect password. Please try again or use fingerprint.");
+ return qsTr("Incorrect password. Please try again.");
+ }
+ if (pam.fprintState === "fail")
+ return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries);
+
+ return "";
+ }
+
+ Layout.fillWidth: true
+ Layout.topMargin: -Appearance.spacing.large
+
+ scale: 0.7
+ opacity: 0
+ color: Colours.palette.m3error
+
+ font.pointSize: Appearance.font.size.small
+ font.family: Appearance.font.family.mono
+ horizontalAlignment: Qt.AlignHCenter
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+
+ onMsgChanged: {
+ if (msg) {
+ if (opacity > 0) {
+ animate = true;
+ text = msg;
+ animate = false;
+
+ exitAnim.stop();
+ if (scale < 1)
+ appearAnim.restart();
+ else
+ flashAnim.restart();
+ } else {
+ text = msg;
+ exitAnim.stop();
+ appearAnim.restart();
+ }
+ } else {
+ appearAnim.stop();
+ flashAnim.stop();
+ exitAnim.start();
+ }
+ }
+
+ Connections {
+ target: root.lock.pam
+
+ function onFlashMsg(): void {
+ exitAnim.stop();
+ if (message.scale < 1)
+ appearAnim.restart();
+ else
+ flashAnim.restart();
+ }
+ }
+
+ Anim {
+ id: appearAnim
+
+ target: message
+ properties: "scale,opacity"
+ to: 1
+ onFinished: flashAnim.restart()
+ }
+
+ SequentialAnimation {
+ id: flashAnim
+
+ loops: 2
+
+ MessageAnim {
+ to: 0.3
+ }
+ MessageAnim {
+ to: 1
+ }
+ }
+
+ ParallelAnimation {
+ id: exitAnim
+
+ Anim {
+ target: message
+ property: "scale"
+ to: 0.7
+ duration: Appearance.anim.durations.large
+ }
+ Anim {
+ target: message
+ property: "opacity"
+ to: 0
+ duration: Appearance.anim.durations.large
+ }
+ }
+ }
+
+ component MessageAnim: NumberAnimation {
+ target: message
+ property: "opacity"
+ duration: Appearance.anim.durations.small
+ easing.type: Easing.Linear
+ }
+
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml
index 6975a89..21c1331 100644
--- a/modules/lock/Content.qml
+++ b/modules/lock/Content.qml
@@ -8,17 +8,12 @@ RowLayout {
id: root
required property var lock
- property real centerScale
-
- anchors.fill: parent
- anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.large * 2
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
- scale: root.centerScale
StyledRect {
Layout.fillWidth: true
@@ -62,16 +57,12 @@ RowLayout {
}
Center {
- Layout.leftMargin: -(1 - scale) * implicitWidth / 2
- Layout.rightMargin: -(1 - scale) * implicitWidth / 2
- scale: root.centerScale
lock: root.lock
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
- scale: root.centerScale
StyledRect {
Layout.fillWidth: true
diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml
index 9472d41..9360a1e 100644
--- a/modules/lock/InputField.qml
+++ b/modules/lock/InputField.qml
@@ -40,19 +40,15 @@ Item {
anchors.centerIn: parent
text: {
- if (root.pam.active)
+ if (root.pam.passwd.active)
return qsTr("Loading...");
- if (root.pam.state === "error")
- return qsTr("An error occured");
if (root.pam.state === "max")
return qsTr("You have reached the maximum number of tries");
- if (root.pam.state === "fail")
- return qsTr("Incorrect password");
return qsTr("Enter your password");
}
animate: true
- color: root.pam.active ? Colours.palette.m3secondary : root.pam.state ? Colours.palette.m3error : Colours.palette.m3outline
+ color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
font.family: Appearance.font.family.mono
diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml
index 625abb5..c0c962c 100644
--- a/modules/lock/LockSurface.qml
+++ b/modules/lock/LockSurface.qml
@@ -14,7 +14,6 @@ WlSessionLockSurface {
required property Pam pam
readonly property alias unlocking: unlockAnim.running
- readonly property bool animating: initAnim.running || unlockAnim.running
color: "transparent"
@@ -44,7 +43,7 @@ WlSessionLockSurface {
}
Anim {
target: content
- property: "centerScale"
+ property: "scale"
to: 0
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
@@ -132,7 +131,7 @@ WlSessionLockSurface {
}
Anim {
target: content
- property: "centerScale"
+ property: "scale"
to: 1
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
@@ -219,8 +218,13 @@ WlSessionLockSurface {
Content {
id: content
+ anchors.centerIn: parent
+ width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2
+ height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2
+
lock: root
opacity: 0
+ scale: 0
}
}
diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml
index 59bed16..bb2fc7b 100644
--- a/modules/lock/Media.qml
+++ b/modules/lock/Media.qml
@@ -19,7 +19,7 @@ Item {
Image {
anchors.fill: parent
- source: root.lock.animating ? "" : (Players.active?.trackArtUrl ?? "")
+ source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml
index 1cfcd28..a986052 100644
--- a/modules/lock/NotifDock.qml
+++ b/modules/lock/NotifDock.qml
@@ -42,7 +42,7 @@ ColumnLayout {
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
- opacity: root.lock.animating || Notifs.list.length > 0 ? 0 : 1
+ opacity: Notifs.list.length > 0 ? 0 : 1
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.large
@@ -51,7 +51,7 @@ ColumnLayout {
asynchronous: true
source: `file://${Quickshell.shellDir}/assets/dino.png`
fillMode: Image.PreserveAspectFit
- sourceSize.width: root.lock.animating ? 0 : clipRect.width * 0.8
+ sourceSize.width: clipRect.width * 0.8
layer.enabled: true
layer.effect: Colouriser {
@@ -84,7 +84,7 @@ ColumnLayout {
clip: true
model: ScriptModel {
- values: root.lock.animating ? [] : [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
+ values: [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
}
delegate: NotifGroup {}
diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml
index 2675405..0186c2f 100644
--- a/modules/lock/Pam.qml
+++ b/modules/lock/Pam.qml
@@ -1,4 +1,6 @@
+import qs.config
import Quickshell
+import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pam
import QtQuick
@@ -9,12 +11,16 @@ Scope {
required property WlSessionLock lock
readonly property alias passwd: passwd
- readonly property bool active: passwd.active
+ readonly property alias fprint: fprint
+ property string lockMessage
property string state
+ property string fprintState
property string buffer
+ signal flashMsg
+
function handleKey(event: KeyEvent): void {
- if (passwd.active)
+ if (passwd.active || state === "max")
return;
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
@@ -34,6 +40,16 @@ Scope {
PamContext {
id: passwd
+ config: "passwd"
+ configDirectory: Quickshell.shellDir + "/assets/pam.d"
+
+ onMessageChanged: {
+ if (message.startsWith("The account is locked"))
+ root.lockMessage = message;
+ else if (root.lockMessage && message.endsWith(" left to unlock)"))
+ root.lockMessage += "\n" + message;
+ }
+
onResponseRequiredChanged: {
if (!responseRequired)
return;
@@ -53,22 +69,125 @@ Scope {
else if (res === PamResult.Failed)
root.state = "fail";
+ root.flashMsg();
stateReset.restart();
}
}
+ PamContext {
+ id: fprint
+
+ property bool available
+ property int tries
+ property int errorTries
+
+ function checkAvail(): void {
+ if (!available || !Config.lock.enableFprint || !root.lock.secure) {
+ abort();
+ return;
+ }
+
+ tries = 0;
+ errorTries = 0;
+ start();
+ }
+
+ config: "fprint"
+ configDirectory: Quickshell.shellDir + "/assets/pam.d"
+
+ onCompleted: res => {
+ if (!available)
+ return;
+
+ if (res === PamResult.Success)
+ return root.lock.unlock();
+
+ if (res === PamResult.Error) {
+ root.fprintState = "error";
+ errorTries++;
+ if (errorTries < 5) {
+ abort();
+ errorRetry.restart();
+ }
+ } else if (res === PamResult.MaxTries) {
+ // Isn't actually the real max tries as pam only reports completed
+ // when max tries is reached.
+ tries++;
+ if (tries < Config.lock.maxFprintTries) {
+ // Restart if not actually real max tries
+ root.fprintState = "fail";
+ start();
+ } else {
+ root.fprintState = "max";
+ abort();
+ }
+ }
+
+ root.flashMsg();
+ fprintStateReset.start();
+ }
+ }
+
+ Process {
+ id: availProc
+
+ command: ["sh", "-c", "fprintd-list $USER"]
+ onExited: code => {
+ fprint.available = code === 0;
+ fprint.checkAvail();
+ }
+ }
+
+ Timer {
+ id: errorRetry
+
+ interval: 800
+ onTriggered: fprint.start()
+ }
+
Timer {
id: stateReset
interval: 4000
- onTriggered: root.state = ""
+ onTriggered: {
+ if (root.state !== "max")
+ root.state = "";
+ }
+ }
+
+ Timer {
+ id: fprintStateReset
+
+ interval: 4000
+ onTriggered: {
+ root.fprintState = "";
+ fprint.errorTries = 0;
+ }
}
Connections {
target: root.lock
+ function onSecureChanged(): void {
+ if (root.lock.secure) {
+ availProc.running = true;
+ root.buffer = "";
+ root.state = "";
+ root.fprintState = "";
+ root.lockMessage = "";
+ }
+ }
+
function onUnlock(): void {
- root.buffer = "";
+ fprint.abort();
+ }
+ }
+
+ Connections {
+ target: Config.lock
+
+ function onEnableFprintChanged(): void {
+ fprint.checkAvail();
}
}
}