feat(qbz-qt): rebuild Settings 1:1 against Slint

The pane was written fast and had drifted both ways: four whole sections were
a placeholder reading "not ported to the Qt frontend yet", and what was there
carried rows and copy Slint does not have — an invented custom-theme info row,
made-up "Restart to apply" descriptions, dividers Integrations never had, and
two buttons forked inline instead of using the shared control.

Every section is real now, in Slint's order and grouping, with Slint's labels
and control types: Audio, Playback, Appearance, Offline, Local Library,
Blacklist, Integrations, Developer, and the sandbox section that only appears
on a Flatpak or Snap install. Local Library brings the folder table and the
Plex panel — 856 lines of Slint with no counterpart here until now — wired to
the Plex store that landed earlier this round. Its PIN auth flow is not ported,
so the manual server-url and token path is what this exposes.

The rule the round was run under: a row whose key is not in settings_qt's
dispatch renders and does nothing, so it is either wired or not shipped.
"Share logs" was exactly that — present since the port began, inert — and now
opens the on-disk log.

The directory is per-section files under a thin composition root rather than
one growing file, and the sub-nav's bottom spacer stops being arithmetic over a
hardcoded row count, which broke the moment anyone added a section.

cc
This commit is contained in:
vicrodh committed 2026-07-28 17:21:44 -06:00
1 parent 75ea846768
commit 83999fc552
19 files changed
+2655 -541

No files matched your search

+294
View File
@@ -0,0 +1,294 @@
// Settings > Local Library > PLEX — the QML port of the PLEX half of
// crates/qbz-ui/ui/settings/LocalLibrarySettings.slint (lines 517-855):
// master toggle + collapse header, server address, token, connection
// actions, the music-library picker, the metadata-write toggle and the Plex
// danger zone.
//
// Wiring: the LIVE state (enabled / available / syncing / sections / error)
// is the QbzLocal bridge's own Plex properties — the SAME ones the Local
// Library browse view reads, so a change here reloads the browse union in
// one hop. The two fields the bridge does not publish (the persisted server
// url and whether a token is stored) ride the settings document
// (settings_qt/library.rs). The token itself NEVER leaves Rust.
//
// Delta vs the Slint: the PIN flow (Authorize / Generate code / Link code /
// Copy code / Open Plex sign-in, and the "Enter token manually" switch that
// chooses between them) is NOT ported — `local_plex.rs` implements the
// manual server-url + token path only, so that path is always shown and the
// PIN rows are absent instead of dead.
import QtQuick
import com.blitzfc.qbz
import "../controls"
import "../theme"
Column {
id: root
property var doc: ({})
readonly property var plex: (doc.library || ({})).plex || ({})
// In-progress field values (the fields commit on Enter / focus loss; the
// Connect button submits whatever is typed right now).
property string urlInput: ""
property string tokenInput: ""
QbzTheme { id: theme }
spacing: 4
readonly property var sections: {
try {
return JSON.parse(QbzLocal.plexSectionsJson)
} catch (e) {
return []
}
}
// Same lazy-load hook as the Slint panel's init: re-publish the Plex
// state (sections + gates) whenever the section becomes visible.
onVisibleChanged: {
if (visible) {
urlInput = plex.serverUrl || ""
QbzLocal.refreshPlex()
}
}
// ===================== header: title + toggle + chevron ================
Item {
width: parent.width
height: 64
Column {
anchors.left: parent.left
anchors.right: plexControls.left
anchors.rightMargin: 24
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: QbzSession.tr("PLEX", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 11
font.letterSpacing: 1.5
font.weight: theme.weightSemibold
}
Text {
width: parent.width
text: QbzSession.tr("Connect a Plex Media Server on your local network.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
wrapMode: Text.WordWrap
}
}
Row {
id: plexControls
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Rectangle {
visible: QbzLocal.plexEnabled
width: 28
height: 28
radius: theme.radiusSm
anchors.verticalCenter: parent.verticalCenter
color: chevArea.containsMouse ? theme.surfaceHover : "transparent"
QbzIcon {
anchors.centerIn: parent
name: root.plex.collapsed === true ? "chevron-right" : "chevron-down"
width: 18
height: 18
tintName: "secondary"
}
MouseArea {
id: chevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzBridge.settingsBool("plex-collapse", root.plex.collapsed !== true)
}
}
QbzToggle {
anchors.verticalCenter: parent.verticalCenter
checked: QbzLocal.plexEnabled
onToggled: function (v) { QbzLocal.plexSetEnabled(v) }
}
}
}
// ============================== body ==================================
Column {
visible: QbzLocal.plexEnabled && root.plex.collapsed !== true
width: parent.width
spacing: 4
Item { width: 1; height: 10 }
SettingRow {
label: QbzSession.tr("Server address", QbzSession.trRev)
description: QbzSession.tr("Only local network servers are supported.", QbzSession.trRev)
QbzLineEdit {
id: urlField
width: 240
text: root.plex.serverUrl || ""
placeholder: "http://127.0.0.1:32400"
onEdited: function (v) { root.urlInput = v }
onCommitted: function (v) { root.urlInput = v }
}
}
SettingRow {
label: QbzSession.tr("Token", QbzSession.trRev)
description: root.plex.hasToken === true
? QbzSession.tr("A token is stored for this server.", QbzSession.trRev)
: QbzSession.tr("Use an existing X-Plex-Token.", QbzSession.trRev)
QbzLineEdit {
id: tokenField
width: 240
isPassword: true
placeholder: QbzSession.tr("X-Plex-Token", QbzSession.trRev)
onEdited: function (v) { root.tokenInput = v }
onCommitted: function (v) { root.tokenInput = v }
}
}
// Connect = persist the credentials + sync (plex_connect resolves the
// url, refuses anything that is not a LAN address, then runs a sync).
SettingRow {
label: QbzSession.tr("Connect", QbzSession.trRev)
description: QbzSession.tr("Saves the address and token, then fetches your libraries.", QbzSession.trRev)
SettingsButton {
text: QbzLocal.plexSyncing
? QbzSession.tr("Working...", QbzSession.trRev)
: QbzSession.tr("Connect", QbzSession.trRev)
enabled: !QbzLocal.plexSyncing
onClicked: {
QbzLocal.plexConnect(root.urlInput !== "" ? root.urlInput
: (root.plex.serverUrl || ""), root.tokenInput)
root.tokenInput = ""
tokenField.text = ""
}
}
}
Item { width: 1; height: 14 }
SettingRow {
label: QbzSession.tr("Libraries", QbzSession.trRev)
description: QbzSession.tr("Fetch your Plex music libraries.", QbzSession.trRev)
SettingsButton {
text: QbzLocal.plexSyncing
? QbzSession.tr("Working...", QbzSession.trRev)
: QbzSession.tr("Get libraries", QbzSession.trRev)
enabled: QbzLocal.plexAvailable && !QbzLocal.plexSyncing
onClicked: QbzLocal.syncPlex()
}
}
// Status line: the last Plex error, or the last sync's track count.
Text {
visible: QbzLocal.plexError !== "" || QbzLocal.plexLastSyncTracks >= 0
width: parent.width
text: QbzLocal.plexError !== ""
? QbzLocal.plexError
: QbzSession.tr("Synced {} tracks", QbzSession.trRev)
.replace("{}", QbzLocal.plexLastSyncTracks)
color: QbzLocal.plexError !== "" ? theme.danger : theme.success
font.pixelSize: theme.fontLegal
wrapMode: Text.WordWrap
}
Item { width: 1; height: 6 }
// ---------------------- music library picker ----------------------
Column {
visible: root.sections.length > 0
width: parent.width
spacing: 2
GroupHeader { text: QbzSession.tr("MUSIC LIBRARIES", QbzSession.trRev) }
Item { width: 1; height: 4 }
Repeater {
model: root.sections
delegate: Rectangle {
id: secRow
required property var modelData
width: parent ? parent.width : 0
height: 42
radius: theme.radiusSm
color: secArea.containsMouse ? theme.surfaceHover : "transparent"
function toggle() {
const keys = []
for (let i = 0; i < root.sections.length; i++) {
const s = root.sections[i]
const on = s.key === secRow.modelData.key ? !s.selected : s.selected
if (on) keys.push(s.key)
}
QbzLocal.setPlexSections(JSON.stringify(keys))
}
MouseArea {
id: secArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: secRow.toggle()
}
Row {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 12
spacing: 12
QbzCheckbox {
anchors.verticalCenter: parent.verticalCenter
checked: secRow.modelData.selected === true
onToggled: secRow.toggle()
}
Text {
width: parent.width - 18 - 24
height: parent.height
text: secRow.modelData.title || ""
color: theme.textPrimary
font.pixelSize: theme.fontBody
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
}
}
Item { width: 1; height: 14 }
}
// -------------------------- metadata write -------------------------
SettingRow {
label: QbzSession.tr("Write metadata to Plex (experimental)", QbzSession.trRev)
description: QbzSession.tr("Allow QBZ to write track metadata back to your Plex server.", QbzSession.trRev)
QbzToggle {
checked: root.plex.metadataWrite === true
onToggled: function (v) { QbzBridge.settingsBool("plex-metadata-write", v) }
}
}
Item { width: 1; height: 18 }
// ------------------------- plex danger zone ------------------------
GroupHeader { text: QbzSession.tr("PLEX DANGER ZONE", QbzSession.trRev) }
SettingRow {
label: QbzSession.tr("Disconnect", QbzSession.trRev)
description: QbzSession.tr("Sign out of Plex and clear the local cache.", QbzSession.trRev)
SettingsButton {
danger: true
text: QbzSession.tr("Disconnect", QbzSession.trRev)
enabled: root.plex.hasToken === true
onClicked: QbzLocal.plexDisconnect()
}
}
SettingRow {
label: QbzSession.tr("Clear cache", QbzSession.trRev)
description: QbzSession.tr("Remove cached Plex libraries and tracks. Your sign-in is kept.", QbzSession.trRev)
SettingsButton {
danger: true
text: QbzSession.tr("Clear cache", QbzSession.trRev)
enabled: root.plex.hasToken === true
onClicked: QbzBridge.settingsString("plex-clear-cache", "")
}
}
}
}