refactor: lock center into multiple files

This commit is contained in:
Soramane 2026-05-28 22:57:23 +08:00
parent 69428059e0
commit 84a82a9e78
6 changed files with 366 additions and 332 deletions

View file

@ -1,13 +1,9 @@
pragma ComponentBehavior: Bound import "center"
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
import qs.components.controls
import qs.components.images
import qs.services import qs.services
import qs.utils
ColumnLayout { ColumnLayout {
id: root id: root
@ -36,336 +32,20 @@ ColumnLayout {
font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build() font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build()
} }
StyledClippingRect { ProfilePic {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
centerWidth: root.centerWidth
implicitWidth: root.centerWidth / 2
implicitHeight: root.centerWidth / 2
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "person"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.size(Math.floor(root.centerWidth / 4)).build()
visible: pfp.status !== Image.Ready
}
CachingImage {
id: pfp
anchors.fill: parent
path: `${Paths.home}/.face`
}
} }
StyledRect { PasswordInput {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
centerWidth: root.centerWidth
implicitWidth: root.centerWidth * 0.8 lock: root.lock
implicitHeight: input.implicitHeight + Tokens.padding.small
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
focus: true
onActiveFocusChanged: {
if (!activeFocus)
forceActiveFocus();
}
Keys.onPressed: event => {
if (root.lock.unlocking)
return;
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)
inputField.placeholder.animate = false;
root.lock.pam.handleKey(event);
}
StateLayer {
onClicked: {
parent.forceActiveFocus();
}
hoverEnabled: false
cursorShape: Qt.IBeamCursor
}
RowLayout {
id: input
anchors.fill: parent
anchors.margins: Tokens.padding.extraSmall
spacing: Tokens.spacing.medium
Item {
implicitWidth: implicitHeight
implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small
MaterialIcon {
id: fprintIcon
anchors.centerIn: parent
animate: true
text: {
if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries)
return "fingerprint_off";
if (root.lock.pam.fprint.active)
return "fingerprint";
return "lock";
}
color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
opacity: root.lock.pam.passwd.active ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
CircularIndicator {
anchors.fill: parent
running: root.lock.pam.passwd.active
}
}
InputField {
id: inputField
pam: root.lock.pam
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: enterIcon.implicitHeight + Tokens.padding.small
color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full
StateLayer {
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
onClicked: root.lock.pam.passwd.start()
}
MaterialIcon {
id: enterIcon
anchors.centerIn: parent
text: "arrow_forward"
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
fontStyle: Tokens.font.icon.size(Tokens.font.icon.large.pointSize).weight(Font.Medium).build()
}
}
}
} }
Item { StateMessage {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: -Tokens.spacing.largeIncreased Layout.topMargin: -Tokens.spacing.largeIncreased
pam: root.lock.pam
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
Behavior on implicitHeight {
Anim {}
}
StyledText {
id: stateMessage
readonly property string msg: {
if (Hypr.kbLayout !== Hypr.defaultKbLayout) {
if (Hypr.capsLock && Hypr.numLock)
return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull);
if (Hypr.capsLock)
return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull);
if (Hypr.numLock)
return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull);
return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull);
}
if (Hypr.capsLock && Hypr.numLock)
return qsTr("Caps lock and Num lock are ON.");
if (Hypr.capsLock)
return qsTr("Caps lock is ON.");
if (Hypr.numLock)
return qsTr("Num lock is ON.");
return "";
}
property bool shouldBeVisible
onMsgChanged: {
if (msg) {
if (opacity > 0) {
animate = true;
text = msg;
animate = false;
} else {
text = msg;
}
shouldBeVisible = true;
} else {
shouldBeVisible = false;
}
}
anchors.left: parent.left
anchors.right: parent.right
scale: shouldBeVisible && !message.msg ? 1 : 0.7
opacity: shouldBeVisible && !message.msg ? 1 : 0
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.mono.small
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
lineHeight: 1.2
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
StyledText {
id: message
readonly property Pam pam: root.lock.pam
readonly property string msg: {
if (pam.fprintState === "error")
return qsTr("FP ERROR: %1").arg(pam.fprint.message);
if (pam.state === "error")
return qsTr("PW 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 "";
}
anchors.left: parent.left
anchors.right: parent.right
scale: 0.7
opacity: 0
color: Colours.palette.m3error
font: Tokens.font.mono.small
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 {
function onFlashMsg(): void {
exitAnim.stop();
if (message.scale < 1)
appearAnim.restart();
else
flashAnim.restart();
}
target: root.lock.pam
}
Anim {
id: appearAnim
type: Anim.DefaultEffects
target: message
properties: "scale,opacity"
to: 1
onFinished: flashAnim.restart()
}
SequentialAnimation {
id: flashAnim
loops: 2
FlashAnim {
to: 0.3
}
FlashAnim {
to: 1
}
}
ParallelAnimation {
id: exitAnim
Anim {
target: message
property: "scale"
to: 0.7
type: Anim.StandardLarge
}
Anim {
target: message
property: "opacity"
to: 0
type: Anim.StandardLarge
}
}
}
}
component FlashAnim: NumberAnimation {
target: message
property: "opacity"
duration: Tokens.anim.durations.small
easing.type: Easing.Linear
} }
} }

View file

@ -1,11 +1,11 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
import qs.services import qs.services
import qs.modules.lock
Item { Item {
id: root id: root
@ -14,9 +14,6 @@ Item {
readonly property alias placeholder: placeholder readonly property alias placeholder: placeholder
property string buffer property string buffer
Layout.fillWidth: true
Layout.fillHeight: true
clip: true clip: true
Connections { Connections {

View file

@ -0,0 +1,112 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
StyledRect {
id: root
required property int centerWidth
required property var lock
implicitWidth: centerWidth * 0.8
implicitHeight: input.implicitHeight + Tokens.padding.small
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
focus: true
onActiveFocusChanged: {
if (!activeFocus)
forceActiveFocus();
}
Keys.onPressed: event => {
if (root.lock.unlocking)
return;
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)
inputField.placeholder.animate = false;
root.lock.pam.handleKey(event);
}
StateLayer {
hoverEnabled: false
cursorShape: Qt.IBeamCursor
onClicked: parent.forceActiveFocus()
}
RowLayout {
id: input
anchors.fill: parent
anchors.margins: Tokens.padding.extraSmall
spacing: Tokens.spacing.medium
Item {
implicitWidth: implicitHeight
implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small
MaterialIcon {
id: fprintIcon
anchors.centerIn: parent
animate: true
text: {
if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries)
return "fingerprint_off";
if (root.lock.pam.fprint.active)
return "fingerprint";
return "lock";
}
color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
opacity: root.lock.pam.passwd.active ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
CircularIndicator {
anchors.fill: parent
running: root.lock.pam.passwd.active
}
}
InputField {
id: inputField
Layout.fillWidth: true
Layout.fillHeight: true
pam: root.lock.pam
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: enterIcon.implicitHeight + Tokens.padding.small
color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full
StateLayer {
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
onClicked: root.lock.pam.passwd.start()
}
MaterialIcon {
id: enterIcon
anchors.centerIn: parent
text: "arrow_forward"
color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
fontStyle: Tokens.font.icon.size(Tokens.font.icon.large.pointSize).weight(Font.Medium).build()
}
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import Caelestia.Config
import qs.components
import qs.components.images
import qs.services
import qs.utils
StyledClippingRect {
id: root
required property int centerWidth
implicitWidth: centerWidth / 2
implicitHeight: centerWidth / 2
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "person"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build()
visible: pfp.status !== Image.Ready
}
CachingImage {
id: pfp
anchors.fill: parent
path: `${Paths.home}/.face`
}
}

View file

@ -0,0 +1,211 @@
pragma ComponentBehavior: Bound
import QtQuick
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.lock
Item {
id: root
required property Pam pam
readonly property string msg: {
if (pam.fprintState === "error")
return qsTr("FP ERROR: %1").arg(pam.fprint.message);
if (pam.state === "error")
return qsTr("PW 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 "";
}
readonly property string stateMsg: {
if (Hypr.kbLayout !== Hypr.defaultKbLayout) {
if (Hypr.capsLock && Hypr.numLock)
return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull);
if (Hypr.capsLock)
return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull);
if (Hypr.numLock)
return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull);
return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull);
}
if (Hypr.capsLock && Hypr.numLock)
return qsTr("Caps lock and Num lock are ON.");
if (Hypr.capsLock)
return qsTr("Caps lock is ON.");
if (Hypr.numLock)
return qsTr("Num lock is ON.");
return "";
}
property bool stateMsgShouldBeVisible
onMsgChanged: {
if (msg) {
if (message.opacity > 0) {
message.animate = true;
message.text = msg;
message.animate = false;
exitAnim.stop();
if (message.scale < 1)
appearAnim.restart();
else
flashAnim.restart();
} else {
message.text = msg;
exitAnim.stop();
appearAnim.restart();
}
} else {
appearAnim.stop();
flashAnim.stop();
exitAnim.start();
}
}
onStateMsgChanged: {
if (stateMsg) {
if (stateMessage.opacity > 0) {
stateMessage.animate = true;
stateMessage.text = stateMsg;
stateMessage.animate = false;
} else {
stateMessage.text = stateMsg;
}
stateMsgShouldBeVisible = true;
} else {
stateMsgShouldBeVisible = false;
}
}
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
Behavior on implicitHeight {
Anim {}
}
StyledText {
id: stateMessage
anchors.left: parent.left
anchors.right: parent.right
scale: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0.7
opacity: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.mono.small
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
lineHeight: 1.2
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
StyledText {
id: message
anchors.left: parent.left
anchors.right: parent.right
scale: 0.7
opacity: 0
color: Colours.palette.m3error
font: Tokens.font.mono.small
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Connections {
function onFlashMsg(): void {
exitAnim.stop();
if (message.scale < 1)
appearAnim.restart();
else
flashAnim.restart();
}
target: root.pam
}
Anim {
id: appearAnim
type: Anim.DefaultEffects
target: message
properties: "scale,opacity"
to: 1
onFinished: flashAnim.restart()
}
SequentialAnimation {
id: flashAnim
loops: 2
FlashAnim {
to: 0.3
}
FlashAnim {
to: 1
}
}
ParallelAnimation {
id: exitAnim
Anim {
target: message
property: "scale"
to: 0.7
type: Anim.StandardLarge
}
Anim {
target: message
property: "opacity"
to: 0
type: Anim.StandardLarge
}
}
}
component FlashAnim: NumberAnimation {
target: message
property: "opacity"
duration: Tokens.anim.durations.small
easing.type: Easing.Linear
}
}