Files
qbz/crates/qbz-qt/qml/settings/LibFolderEditModal.qml
T
vicrodh 461238bd6f fix(qt): modal forms open clean, never with the last session's text
QbzLineEdit re-seeds its box only when `text` CHANGES and only while the
box is not focused; QbzTextArea only while unfocused. Modals that reset
their drafts on the close edge (the box still holds focus for that
frame) or reopen with the seed they closed on produced neither, so the
box kept the previous input while the draft said otherwise. Both
controls gain `reset(value)`, which writes the box directly, and every
modal form calls it from its open handler: playlist edit and create,
MyQBZ create / rename / description, folder create and edit, the link
resolver, the rip wizard and the library folder editor. The MyQBZ
create name now resets on the open edge, and its edit panel re-seeds on
every open, also for the same collection.

Renaming a Qobuz playlist also patches the playlist manager's warm cache,
which the editor seeds from before any fetch: reopening the editor right
after Save showed the pre-save name until the background reload landed,
and forever while offline.

cc
2026-09-14 05:22:44 -06:00

492 lines
21 KiB
QML

// Per-folder settings for a registered Local Library folder — QML port of
// crates/qbz-ui/ui/settings/LibFolderEditModal.slint (itself the Tauri
// FolderSettingsModal). Alias, enabled, network override + fs-type,
// accessibility status, change-path, last-scanned, scan-this-folder.
//
// THE FIELD THAT MATTERS: `userOverrideNetwork`. It has a real consumer in
// the SHARED scanner (`qbz-library/src/scan.rs:164`), where it suppresses
// per-scan network re-detection. Until this modal existed the Qt build had no
// writer for it, so a user who classified a folder by hand had that decision
// silently overwritten by detection on the next scan. This is not a cosmetic
// port.
//
// State rides the settings document (`doc.library.folderEdit`) rather than a
// bridge of its own — it is a surface of the Local Library panel and all its
// actions already land in settings_qt/library.rs. Draft edits are LOCAL to
// this file until Save, so Cancel really cancels; only the accessibility
// probe and the path change write back before then (both come FROM Rust).
//
// MOUNTED AT THE SETTINGS VIEW ROOT, beside SettingsConfirmHost — the panels
// are Columns inside a Flickable, so a modal mounted in one would be sized by
// the scrolled content and ride the scroll. ADR-009: z >= 3000.
import QtQuick
import com.blitzfc.qbz
import "../controls"
import "../theme"
Item {
property bool kioskHost: false
id: root
property var doc: ({})
readonly property var st: (doc.library && doc.library.folderEdit) || ({})
anchors.fill: parent
z: 3100
visible: root.st.open === true
enabled: root.visible
QbzTheme { id: theme }
// The option list and its stored labels, in the reference's order
// (LibFolderEditModal.slint:266-278 / :37-48). Index 0 = auto-detect.
readonly property var fsTypeValues: [
"auto", "cifs", "nfs", "sshfs", "rclone", "webdav", "glusterfs", "ceph", "other"
]
// --- Draft state ----------------------------------------------------
// Seeded on every open, never bound: a binding onto the document would
// snap the user's half-typed alias back on the next publish (the probe
// alone republishes twice per open).
property string draftAlias: ""
property bool draftEnabled: true
property bool draftIsNetwork: false
property bool draftOverride: false
property int draftFsIndex: 0
onVisibleChanged: {
if (!visible)
return
root.draftAlias = root.st.alias || ""
root.draftEnabled = root.st.enabled === true
root.draftIsNetwork = root.st.isNetwork === true
root.draftOverride = root.st.userOverrideNetwork === true
root.draftFsIndex = root.st.fsTypeIndex || 0
aliasField.reset(root.draftAlias)
closeButton.forceActiveFocus()
}
function close() {
QbzBridge.settingsString("library-folder-edit-close", "")
}
function save() {
QbzBridge.settingsString("library-folder-edit-save", JSON.stringify({
id: root.st.folderId || 0,
alias: root.draftAlias,
enabled: root.draftEnabled,
isNetwork: root.draftIsNetwork,
fsType: root.fsTypeValues[root.draftFsIndex] || "auto",
userOverrideNetwork: root.draftOverride
}))
}
Keys.onEscapePressed: function (event) {
root.close()
event.accepted = true
}
// Scrim — click dismisses.
Rectangle {
anchors.fill: parent
color: "#bf000000"
MouseArea {
anchors.fill: parent
onClicked: root.close()
// Wheel-lock (the DiscoverConfigModal rule).
onWheel: function (wheel) { wheel.accepted = true }
}
}
// Faked 32px drop shadow (#00000080) — the LoginScreen/QbzConfirmModal
// precedent; a real blurred shadow owes its own parity pass.
Rectangle {
anchors.centerIn: panel
width: panel.width + 8
height: panel.height + 8
radius: theme.radiusMd
color: "#80000000"
z: -1
}
Rectangle {
id: panel
anchors.centerIn: parent
width: Math.min(root.width - 80, 520)
height: Math.min(root.height - 80, body.implicitHeight + 44)
radius: theme.radiusMd
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
// Swallow clicks so they never reach the scrim.
MouseArea {
anchors.fill: parent
// Wheel-lock (the DiscoverConfigModal rule).
onWheel: function (wheel) { wheel.accepted = true }
}
Flickable {
anchors.fill: parent
clip: root.kioskHost
interactive: root.kioskHost
contentWidth: width
contentHeight: body.implicitHeight + 44
boundsBehavior: Flickable.StopAtBounds
Column {
id: body
x: 22
y: 22
width: parent.width - 44
spacing: 16
// --- Title + close ------------------------------------------
Item {
width: parent.width
height: root.kioskHost ? 44 : 28
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Folder settings", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: root.kioskHost ? (theme.fontHeading) * 1.2 : (theme.fontHeading)
font.weight: theme.weightSemibold
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: root.kioskHost ? 44 : 28
height: root.kioskHost ? 44 : 28
radius: theme.radiusSm
color: closeArea.containsMouse ? theme.surfaceHover : "transparent"
activeFocusOnTab: root.visible
border.width: activeFocus ? 2 : 0
border.color: theme.accent
Accessible.role: Accessible.Button
Accessible.name: QbzSession.tr("Close", QbzSession.trRev)
Accessible.onPressAction: root.close()
Keys.onPressed: function (event) {
if (!event.isAutoRepeat
&& (event.key === Qt.Key_Space
|| event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)) {
root.close()
event.accepted = true
}
}
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 17
height: 17
tintName: closeArea.containsMouse ? "primary" : "muted"
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: closeButton.forceActiveFocus()
onClicked: root.close()
}
}
}
// --- Folder location + Change + accessibility ---------------
Column {
width: parent.width
spacing: 6
Text {
text: QbzSession.tr("Folder location", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
font.weight: theme.weightSemibold
}
Item {
width: parent.width
height: 32
QbzIcon {
id: locIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: root.draftIsNetwork ? "network" : "hard-drive"
width: 18
height: 18
tintName: root.draftIsNetwork ? "accent" : "secondary"
}
Text {
anchors.left: locIcon.right
anchors.leftMargin: 10
anchors.right: changeBtn.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: root.st.path || ""
color: theme.textSecondary
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
elide: Text.ElideMiddle
}
SettingsButton { kioskHost: root.kioskHost;
id: changeBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
iconName: "folder-open"
text: QbzSession.tr("Change", QbzSession.trRev)
onClicked: QbzBridge.settingsString(
"library-folder-change-path", String(root.st.folderId || 0))
}
}
// Accessibility status. `checkingAccessible` is true while the
// Rust probe is out — it starts that way on every open, so a
// dead mount does not hold the modal closed.
Row {
spacing: 8
QbzIcon {
visible: root.st.checkingAccessible !== true
anchors.verticalCenter: parent.verticalCenter
name: root.st.accessible === true ? "check" : "x"
width: 14
height: 14
// The reference hard-codes #3fae6a / #e0564f. QbzIcon
// tints from a baked SET, not an arbitrary colour, so
// these go through the port's two documented
// substitutions: "green" (#22c55e) and "favorite"
// (#ef4444, which icon_tint_qt.rs:63 already records
// as the port-wide stand-in for #e0564f danger).
tintName: root.st.accessible === true ? "green" : "favorite"
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.st.checkingAccessible === true
? QbzSession.tr("Checking...", QbzSession.trRev)
: (root.st.accessible === true
? QbzSession.tr("Folder is accessible", QbzSession.trRev)
: QbzSession.tr("Folder is not accessible", QbzSession.trRev))
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
}
}
}
// --- Display name -------------------------------------------
Column {
width: parent.width
spacing: 6
Text {
text: QbzSession.tr("Display name", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
font.weight: theme.weightSemibold
}
QbzLineEdit { kioskHost: root.kioskHost;
id: aliasField
width: parent.width
placeholder: QbzSession.tr("Optional friendly name", QbzSession.trRev)
onEdited: function (v) { root.draftAlias = v }
onCommitted: function (v) { root.draftAlias = v }
}
Text {
text: QbzSession.tr("Shown instead of the full path in the folder list.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
}
}
// --- Enabled -------------------------------------------------
Item {
width: parent.width
height: 40
Column {
anchors.left: parent.left
anchors.right: enabledToggle.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: QbzSession.tr("Enabled", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
}
Text {
width: parent.width
text: QbzSession.tr("Disabled folders are skipped during scans.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
wrapMode: Text.WordWrap
}
}
QbzToggle { kioskHost: root.kioskHost;
id: enabledToggle
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: root.draftEnabled
onToggled: function (v) { root.draftEnabled = v }
}
}
// --- Network override ----------------------------------------
// Flipping this sets userOverrideNetwork, which is what tells the
// shared scanner to stop re-detecting (scan.rs:164).
Item {
width: parent.width
height: 40
Column {
anchors.left: parent.left
anchors.right: netToggle.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: QbzSession.tr("Network folder", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
}
Text {
width: parent.width
text: QbzSession.tr("Mark this folder as a network share (excluded when offline).", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
wrapMode: Text.WordWrap
}
}
QbzToggle { kioskHost: root.kioskHost;
id: netToggle
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: root.draftIsNetwork
onToggled: function (v) {
root.draftIsNetwork = v
root.draftOverride = true
}
}
}
// --- FS type (network only) ----------------------------------
Column {
width: parent.width
spacing: 6
visible: root.draftIsNetwork
Text {
text: QbzSession.tr("Network filesystem type", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
font.weight: theme.weightSemibold
}
QbzSelect { kioskHost: root.kioskHost;
menuWidth: 200
options: [
QbzSession.tr("Auto-detect", QbzSession.trRev),
"CIFS / SMB", "NFS", "SSHFS", "rclone", "WebDAV",
"GlusterFS", "Ceph",
QbzSession.tr("Other", QbzSession.trRev)
]
currentIndex: root.draftFsIndex
onSelected: function (i) { root.draftFsIndex = i }
}
}
// --- Last scanned --------------------------------------------
Item {
width: parent.width
height: 18
Text {
anchors.left: parent.left
text: QbzSession.tr("Last scanned", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
}
Text {
anchors.right: parent.right
text: (root.st.lastScan || 0) > 0
? new Date((root.st.lastScan || 0) * 1000).toLocaleString(Qt.locale())
: QbzSession.tr("Never", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
}
}
Rectangle {
width: parent.width
height: 1
color: theme.borderSubtle
}
// --- Footer ---------------------------------------------------
Item {
width: parent.width
height: 36
// Scan needs the folder to be both enabled AND reachable —
// scanning a disabled or dead folder is a guaranteed no-op.
SettingsButton { kioskHost: root.kioskHost;
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
iconName: "refresh-cw"
text: QbzSession.tr("Scan this folder", QbzSession.trRev)
enabled: root.draftEnabled && root.st.accessible === true
onClicked: {
QbzBridge.settingsString("library-scan", String(root.st.folderId || 0))
root.close()
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 10
SettingsButton { kioskHost: root.kioskHost;
text: QbzSession.tr("Cancel", QbzSession.trRev)
onClicked: root.close()
}
// Accent-FILLED primary. Built inline rather than by
// adding an `accent` arm to the shared SettingsButton:
// that control is outlined-only everywhere it is used
// today, and QbzConfirmModal already draws its primary
// this same way (:218-232), including the
// `accentGlyphColor` selector for the label.
Rectangle {
id: saveButton
anchors.verticalCenter: parent.verticalCenter
width: saveLabel.implicitWidth + 36
height: 36
radius: theme.radiusSm
color: saveArea.containsMouse ? theme.accentHover : theme.accent
activeFocusOnTab: root.visible
border.width: activeFocus ? 2 : 0
border.color: theme.accentGlyphColor
Accessible.role: Accessible.Button
Accessible.name: QbzSession.tr("Save", QbzSession.trRev)
Accessible.onPressAction: root.save()
Keys.onPressed: function (event) {
if (!event.isAutoRepeat
&& (event.key === Qt.Key_Space
|| event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)) {
root.save()
event.accepted = true
}
}
Text {
id: saveLabel
anchors.centerIn: parent
text: QbzSession.tr("Save", QbzSession.trRev)
color: theme.accentGlyphColor
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
font.weight: theme.weightSemibold
}
MouseArea {
id: saveArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: saveButton.forceActiveFocus()
onClicked: root.save()
}
}
}
}
}
}
}
}