diff --git a/crates/qbz-qt/qml/controls/FolderEditPanel.qml b/crates/qbz-qt/qml/controls/FolderEditPanel.qml index 1075750cd..b564c3846 100644 --- a/crates/qbz-qt/qml/controls/FolderEditPanel.qml +++ b/crates/qbz-qt/qml/controls/FolderEditPanel.qml @@ -95,6 +95,8 @@ Item { root.draftPreset = root.doc.iconPreset || "folder" root.draftColor = root.doc.iconColor || "" root.draftHidden = root.doc.isHidden === true + // The box as well as the draft (QbzLineEdit.reset()). + nameField.reset() } /// Focus the name field. Split out so the host can call it AFTER the panel diff --git a/crates/qbz-qt/qml/controls/FolderModals.qml b/crates/qbz-qt/qml/controls/FolderModals.qml index b25432249..d49064a6a 100644 --- a/crates/qbz-qt/qml/controls/FolderModals.qml +++ b/crates/qbz-qt/qml/controls/FolderModals.qml @@ -103,6 +103,7 @@ Item { if (root.createOpen) { // Its OWN draft, reset on its OWN open transition. root.draftCreateName = "" + createField.reset("") createScope.forceActiveFocus() createField.focusField() } else { diff --git a/crates/qbz-qt/qml/controls/LinkResolverModal.qml b/crates/qbz-qt/qml/controls/LinkResolverModal.qml index 6d328a7a5..77133b299 100644 --- a/crates/qbz-qt/qml/controls/LinkResolverModal.qml +++ b/crates/qbz-qt/qml/controls/LinkResolverModal.qml @@ -25,6 +25,9 @@ Item { onVisibleChanged: { if (visible) { + // Rust cleared `url` before opening; sync the box regardless of + // where focus sat at the last close (QbzLineEdit.reset()). + urlInput.reset(QbzLink.url) urlInput.focusField() } else { // Return focus only if it still belongs to this modal. A search diff --git a/crates/qbz-qt/qml/controls/MyQbzModals.qml b/crates/qbz-qt/qml/controls/MyQbzModals.qml index 139236af4..29b0facb7 100644 --- a/crates/qbz-qt/qml/controls/MyQbzModals.qml +++ b/crates/qbz-qt/qml/controls/MyQbzModals.qml @@ -76,10 +76,19 @@ Item { property string name: "" readonly property bool canCreate: createPanel.name !== "" && !createPanel.creating - // A fresh open starts with an empty field. + // A fresh open starts with an empty field — reset on the OPEN edge, + // never the close edge: at close the box still holds focus for a + // frame, its focus-gated re-seed skips, and the next open found the + // draft already "" with the old text still in the box (the + // dirty-form report of 2026-09-14). `reset()` syncs the box itself. Connections { target: root - function onCreateOpenChanged() { createPanel.name = "" } + function onCreateOpenChanged() { + if (!root.createOpen) + return + createPanel.name = "" + createNameField.reset("") + } } Rectangle { @@ -166,6 +175,7 @@ Item { font.letterSpacing: 1.5 } QbzLineEdit { + id: createNameField width: parent.width height: 34 text: createPanel.name @@ -255,10 +265,17 @@ Item { // frame, and then `open` never changes and the prefill would be stale. // Keying it on the whole document instead would clobber what the user // is typing every time an unrelated field (`busy`) flips. - readonly property string editKey: editPanel.mode + "|" + (root.editDoc.name || "") + // The open flag rides along so a reopen on the SAME collection + // re-seeds too (a cancelled rename left its draft behind). + readonly property string editKey: (root.editOpen ? "1" : "0") + "|" + + editPanel.mode + "|" + (root.editDoc.name || "") onEditKeyChanged: { editPanel.draftName = root.editDoc.name || "" editPanel.draftDescription = root.editDoc.description || "" + if (root.editOpen) { + renameField.reset() + descriptionField.reset() + } } Rectangle { @@ -338,6 +355,7 @@ Item { // --- Body: rename QbzLineEdit { + id: renameField visible: editPanel.isRename width: parent.width height: visible ? 34 : 0 @@ -350,6 +368,7 @@ Item { // --- Body: description QbzTextArea { + id: descriptionField visible: editPanel.isDescription width: parent.width // A FLAT 96, never `visible ? 96 : 0`: a Column does not diff --git a/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml b/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml index 3209e23bf..d2eaba1cd 100644 --- a/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml +++ b/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml @@ -103,6 +103,10 @@ Item { root.draftIsPublic = false // Offline forces it ON and locks it (reference :184-216). root.draftOfflineOnly = root.offlineLocked + // The boxes too (QbzLineEdit.reset()): "" is also what the + // drafts held after the last cancel, so nothing above re-seeds. + nameField.reset("") + descriptionField.reset("") scope.forceActiveFocus() nameField.focusField() } else { @@ -274,6 +278,7 @@ Item { font.weight: theme.weightMedium } QbzTextArea { + id: descriptionField width: parent.width height: 80 text: root.draftDescription diff --git a/crates/qbz-qt/qml/controls/PlaylistEditModal.qml b/crates/qbz-qt/qml/controls/PlaylistEditModal.qml index b1b580efd..449518fd5 100644 --- a/crates/qbz-qt/qml/controls/PlaylistEditModal.qml +++ b/crates/qbz-qt/qml/controls/PlaylistEditModal.qml @@ -135,6 +135,11 @@ Item { root.draftDescription = root.doc.description || "" root.draftOfflineOnly = root.doc.offlineOnly === true root.draftFolderId = root.doc.folderId || "" + // The boxes, not just the drafts: a seed equal to the previous + // one changes nothing above, and a box that was still focused + // at close never re-seeded (QbzLineEdit.reset()). + nameField.reset() + descriptionField.reset() scope.forceActiveFocus() nameField.focusField() } @@ -332,6 +337,7 @@ Item { font.pixelSize: theme.fontLegal } QbzTextArea { + id: descriptionField width: parent.width height: 90 text: root.draftDescription diff --git a/crates/qbz-qt/qml/controls/QbzLineEdit.qml b/crates/qbz-qt/qml/controls/QbzLineEdit.qml index 5eda4ce41..2122fd7e4 100644 --- a/crates/qbz-qt/qml/controls/QbzLineEdit.qml +++ b/crates/qbz-qt/qml/controls/QbzLineEdit.qml @@ -101,6 +101,20 @@ Rectangle { property bool _cleared: false onTextChanged: root._cleared = false + /// Re-seed the box from `value` (default: `text`) NOW, focus or not. + /// + /// The re-seed below reacts only to a CHANGE of `text`, and only while + /// the box is not being edited. A modal that resets its draft on the + /// CLOSE edge (the input still holds focus for that frame), or that + /// reopens with the same seed it closed on, produces neither — and the + /// box kept the last thing typed while the draft said otherwise (the + /// dirty-form report of 2026-09-14). Every modal form calls this from + /// its open handler, after seeding its drafts. + function reset(value) { + root._cleared = false + input.text = (value === undefined) ? root.text : String(value) + } + function clearSearch() { input.text = "" root._cleared = true diff --git a/crates/qbz-qt/qml/controls/QbzTextArea.qml b/crates/qbz-qt/qml/controls/QbzTextArea.qml index 67bbe2c57..169cfdbed 100644 --- a/crates/qbz-qt/qml/controls/QbzTextArea.qml +++ b/crates/qbz-qt/qml/controls/QbzTextArea.qml @@ -28,6 +28,12 @@ Rectangle { property int wrapMode: TextEdit.WordWrap signal edited(string value) + /// Re-seed the body from `value` (default: `text`) NOW, focus or not — + /// the QbzLineEdit.reset() twin (the why is documented there). + function reset(value) { + input.text = (value === undefined) ? root.text : String(value) + } + QbzTheme { id: theme } width: 240 diff --git a/crates/qbz-qt/qml/controls/RipWizardModal.qml b/crates/qbz-qt/qml/controls/RipWizardModal.qml index 7acbee6a2..2d08f9484 100644 --- a/crates/qbz-qt/qml/controls/RipWizardModal.qml +++ b/crates/qbz-qt/qml/controls/RipWizardModal.qml @@ -75,6 +75,11 @@ Item { album = doc.album || "" albumArtist = doc.albumArtist || "" year = doc.year || "" + // The boxes too (QbzLineEdit.reset()): a disc re-read with the same + // tags changes no draft above. + albumField.reset() + yearField.reset() + albumArtistField.reset() libraryChoice = "none" var copy = [] var src = doc.tracks || [] @@ -254,6 +259,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: albumField text: root.album onEdited: function (v) { root.album = v } } @@ -265,6 +271,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: yearField text: root.year onEdited: function (v) { root.year = v } } @@ -277,6 +284,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: albumArtistField text: root.albumArtist onEdited: function (v) { root.albumArtist = v } } diff --git a/crates/qbz-qt/qml/settings/LibFolderEditModal.qml b/crates/qbz-qt/qml/settings/LibFolderEditModal.qml index 20bf749b1..eb3f63a16 100644 --- a/crates/qbz-qt/qml/settings/LibFolderEditModal.qml +++ b/crates/qbz-qt/qml/settings/LibFolderEditModal.qml @@ -64,7 +64,7 @@ Item { root.draftIsNetwork = root.st.isNetwork === true root.draftOverride = root.st.userOverrideNetwork === true root.draftFsIndex = root.st.fsTypeIndex || 0 - aliasField.text = root.draftAlias + aliasField.reset(root.draftAlias) closeButton.forceActiveFocus() } diff --git a/crates/qbz-qt/src/playlist_qt.rs b/crates/qbz-qt/src/playlist_qt.rs index b19d6bf51..393b842d8 100644 --- a/crates/qbz-qt/src/playlist_qt.rs +++ b/crates/qbz-qt/src/playlist_qt.rs @@ -1874,6 +1874,26 @@ pub async fn rename_by_id( .await .map_err(|e| format!("rename playlist {pid} failed: {e}"))?; + // The editor seeds a Qobuz playlist from the manager's WARM cache before + // it fetches (`playlist_manager_qt::cached_playlist_seed`), and the + // reload `after_write` orders lands later — or never, offline. Patch the + // cache now, so an editor reopened right after Save never shows the + // pre-save name or description (the dirty-form report of 2026-09-14). + { + let cache_name = name.clone(); + let cache_desc = description.map(|d| d.trim().to_string()); + if crate::playlist_manager_qt::patch_cache(|data| { + if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { + p.name = cache_name; + if let Some(desc) = cache_desc { + p.description = Some(desc); + } + } + }) { + crate::playlist_manager_qt::publish_document(); + } + } + let target = pid.to_string(); let patched = with_doc(|d| { if d.id != target {