diff options
| -rw-r--r-- | theme/components/Credentials.qml | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/theme/components/Credentials.qml b/theme/components/Credentials.qml new file mode 100644 index 0000000..c2aba29 --- /dev/null +++ b/theme/components/Credentials.qml @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// One credential slot, two stages. The username shows first; confirming it +// slides the username out and the password in. Confirming the password emits +// submitted(). A back chevron, or Esc, returns to the username. + +import QtQuick +import QtQuick.Controls.Basic + +Item { + id: creds + + // Emitted when the password stage is confirmed, and on input so the panel + // can restart its idle timer. + signal submitted() + signal wake() + + property string stage: "user" + readonly property string username: usernameField.text + readonly property string password: passwordField.text + readonly property bool passwordStage: stage === "pass" + + implicitHeight: 44 + + function confirm() { + if (!creds.passwordStage) { + if (usernameField.text.length > 0) + creds.stage = "pass" + } else { + creds.submitted() + } + } + + function clearPassword() { + passwordField.text = "" + } + + function focusStage() { + if (creds.passwordStage) + passwordField.forceActiveFocus() + else + usernameField.forceActiveFocus() + } + + onStageChanged: focusStage() + Component.onCompleted: focusStage() + + // Back chevron, password stage only. The width animates so the slot grows + // and shrinks with it. + Item { + id: back + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: creds.passwordStage ? 44 : 0 + height: 44 + clip: true + visible: width > 0 + + Rectangle { + anchors.fill: parent + radius: 6 + color: config.stringValue("surface0") + opacity: backMouse.containsMouse ? 1 : 0 + } + + Text { + anchors.centerIn: parent + text: "\u2190" + color: config.stringValue("subtext0") + font.family: "Noto Sans" + font.pixelSize: 22 + } + + MouseArea { + id: backMouse + anchors.fill: parent + hoverEnabled: true + onClicked: { + creds.clearPassword() + creds.stage = "user" + } + } + } + + Button { + id: confirmButton + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 44 + height: 44 + padding: 0 + background: Rectangle { + radius: 6 + color: config.stringValue("accent") + } + contentItem: Text { + text: "\u2192" + color: config.stringValue("base") + font.family: "Noto Sans" + font.pixelSize: 22 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + onClicked: creds.confirm() + } + + // The sliding slot. Both fields live here, clipped, and slide across. + Item { + id: slot + anchors.left: back.right + anchors.leftMargin: 8 + anchors.right: confirmButton.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 44 + clip: true + + TextField { + id: usernameField + width: slot.width + height: slot.height + text: userModel.lastUser !== undefined ? userModel.lastUser : "" + placeholderText: "Username" + font.family: "Noto Sans" + color: config.stringValue("text") + palette.base: config.stringValue("surface0") + palette.text: config.stringValue("text") + palette.highlight: config.stringValue("accent") + palette.highlightedText: config.stringValue("base") + background: Rectangle { + radius: 6 + color: config.stringValue("surface0") + } + x: creds.passwordStage ? -slot.width : 0 + opacity: creds.passwordStage ? 0 : 1 + Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } + Behavior on opacity { NumberAnimation { duration: 220 } } + onAccepted: creds.confirm() + onTextChanged: creds.wake() + Keys.onPressed: event => { + creds.wake() + event.accepted = false + } + } + + TextField { + id: passwordField + width: slot.width + height: slot.height + echoMode: TextInput.Password + placeholderText: "Password" + font.family: "Noto Sans" + color: config.stringValue("text") + palette.base: config.stringValue("surface0") + palette.text: config.stringValue("text") + palette.highlight: config.stringValue("accent") + palette.highlightedText: config.stringValue("base") + background: Rectangle { + radius: 6 + color: config.stringValue("surface0") + } + x: creds.passwordStage ? 0 : slot.width + opacity: creds.passwordStage ? 1 : 0 + Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } + Behavior on opacity { NumberAnimation { duration: 220 } } + onAccepted: creds.confirm() + Keys.onPressed: event => { + creds.wake() + if (event.key === Qt.Key_Escape) { + creds.clearPassword() + creds.stage = "user" + event.accepted = true + } else { + event.accepted = false + } + } + } + } +} |
