aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 10:03:17 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 10:03:17 +0200
commit147247a5aa30cae53c1152c4cc15226338def2f6 (patch)
tree5919f0c829b9ccc6a91300177d432397292b7cdb
parent66989fc6cdd5358131d0707bccb314e0d3050d1b (diff)
downloadquickshell-147247a5aa30cae53c1152c4cc15226338def2f6.tar.gz
quickshell-147247a5aa30cae53c1152c4cc15226338def2f6.zip
fix(user): the page retries partial saves and survives leaving mid-save
A key whose write half-failed (AccountsService took it, git did not) reloads as unchanged and Save went dead. A key with an error now counts as changed, and editing a field clears its error. Processes were parented to the page, so leaving it mid-save killed userctl between stores. They hang off the module now, passed in as owner (not the module's id, which would bind to itself). busy now clears when the reload lands, so Save cannot be clicked again before saved is fresh, and the fields and avatar picker are disabled while busy. Avatar paths with # or % broke the preview under a bare file:// prefix; each segment is now URI-encoded for the Image source (page and row), while draft.avatar stays a plain path for userctl. Dropped a redundant saved clear in load. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-rw-r--r--desktop/modules/user/UserModule.qml3
-rw-r--r--desktop/modules/user/UserPage.qml17
-rw-r--r--desktop/modules/user/UserRow.qml2
3 files changed, 15 insertions, 7 deletions
diff --git a/desktop/modules/user/UserModule.qml b/desktop/modules/user/UserModule.qml
index ef49829..9e5210c 100644
--- a/desktop/modules/user/UserModule.qml
+++ b/desktop/modules/user/UserModule.qml
@@ -14,6 +14,7 @@ import "../.."
// The user's details. No tile: the drawer's greeting row opens the page.
Module {
+ id: um
name: "user"
label: "User"
// nf-fa-user, present in Inconsolata Nerd Font.
@@ -23,7 +24,7 @@ Module {
page: Component {
Page {
title: "User"
- UserPage { width: parent.width }
+ UserPage { width: parent.width; owner: um }
}
}
}
diff --git a/desktop/modules/user/UserPage.qml b/desktop/modules/user/UserPage.qml
index 024a63b..f559538 100644
--- a/desktop/modules/user/UserPage.qml
+++ b/desktop/modules/user/UserPage.qml
@@ -43,13 +43,19 @@ Column {
property var keys: []
property var locales: []
property bool busy: false
+ // The module, which outlives the page: a save in flight finishes even if
+ // the page is closed. Not named `um`, the module's id (it would bind to itself).
+ required property QtObject owner
property string loadError: ""
- readonly property var changed: Object.keys(draft).filter(k => draft[k] !== saved[k])
+ readonly property var changed: Object.keys(draft).filter(k => draft[k] !== saved[k] || !!errors[k])
function set(key, value) {
const d = Object.assign({}, form.draft);
d[key] = value;
form.draft = d;
+ const e = Object.assign({}, form.errors);
+ delete e[key];
+ form.errors = e;
}
// ComboBox entries per kind. The current language is kept even if
@@ -67,7 +73,7 @@ Column {
// One Process per call, so reads and writes never share one (a running
// Process ignores a second start, AGENTS.md). cb gets stdout.
function run(cmd, cb) {
- op.createObject(form, { command: cmd, cb: cb }).running = true;
+ op.createObject(form.owner, { command: cmd, cb: cb }).running = true;
}
Component {
id: op
@@ -87,12 +93,12 @@ Column {
function load(keep) {
form.run([form.ctl, "get"], out => {
const s = U.parse(out);
+ form.busy = false;
if (s.error) {
form.loadError = s.error;
return;
}
form.loadError = "";
- form.saved = ({});
form.saved = s;
form.draft = Object.assign({}, s, keep);
});
@@ -106,7 +112,6 @@ Column {
}
function step(queue, failed) {
if (queue.length === 0) {
- form.busy = false;
form.load(failed);
return;
}
@@ -147,7 +152,7 @@ Column {
Image {
id: pic
anchors.fill: parent
- source: form.draft.avatar ? "file://" + form.draft.avatar : ""
+ source: form.draft.avatar ? "file://" + form.draft.avatar.split("/").map(encodeURIComponent).join("/") : ""
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(192, 192)
cache: false
@@ -161,6 +166,7 @@ Column {
}
MouseArea {
anchors.fill: parent
+ enabled: !form.busy
cursorShape: Qt.PointingHandCursor
onClicked: picker.open()
}
@@ -188,6 +194,7 @@ Column {
delegate: Column {
id: field
+ enabled: !form.busy
required property var modelData
width: form.width
spacing: 4
diff --git a/desktop/modules/user/UserRow.qml b/desktop/modules/user/UserRow.qml
index 106a490..bd1fe0b 100644
--- a/desktop/modules/user/UserRow.qml
+++ b/desktop/modules/user/UserRow.qml
@@ -59,7 +59,7 @@ Rectangle {
Image {
id: pic
anchors.fill: parent
- source: row.info.avatar ? "file://" + row.info.avatar : ""
+ source: row.info.avatar ? "file://" + row.info.avatar.split("/").map(encodeURIComponent).join("/") : ""
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(88, 88)
cache: false