feat(myqbz): collapsible My QBZ sidebar with per-element hide and toolbar

Opt-in collapsible My QBZ tree in the sidebar (Settings > Navigation): a
branding-titled parent over Mixtapes and Collections sections, each header a
link to its grid with a chevron that only collapses. Header text and icon on
the left, a Playlists-parity toolbar floating right (search, new, sort A-Z/Z-A,
refresh, collapse) with the chevron aligned to the playlist carets. The tree
feed rides QbzShell (QbzMyQbz is not reliably registered in the shell context)
and is published through publish_sidebar, so it lands reliably at startup.

Per-element visibility reuses the mixtape hidden flag (repo::set_hidden): a
row's right-click hides it from the sidebar, and the detail hero menu toggles
"Hide from sidebar" / "Show in sidebar". Collapse state for the playlist tree
and all three My QBZ sections is persisted. The collapsible tree and the flat
My QBZ entry are mutually exclusive — the classic row is suppressed whenever the
tree is on. Settings gains a Navigation section (Navigation + Search moved out
of Appearance). Detail view also gains a discreet, column-aligned sub-header
over the inline track list. Eight locales updated.

cc
This commit is contained in:
vicrodh committed 2026-09-10 20:19:48 -06:00
1 parent 66cd5fb1da
commit e402fa250a
22 files changed
+1375 -110

No files matched your search

@@ -192,6 +192,13 @@ Rectangle {
readonly property bool loading: doc.loading === true
readonly property bool found: doc.found === true
// The collapsible My QBZ opt-in gates the sidebar hide/show toggle in the
// hero overflow — the concept is meaningless while the flat entry is used.
// GUARDED parse, the NavFlyout.qml:65-70 precedent.
readonly property bool collapsibleMyqbzOn: {
try { return JSON.parse(QbzBridge.settingsJson).collapsibleMyqbz === true }
catch (e) { return false }
}
readonly property string kind: doc.kind || ""
readonly property int itemCount: doc.itemCount || 0
readonly property string viewMode: doc.viewMode || "list"
@@ -816,6 +823,45 @@ Rectangle {
font.pixelSize: 12
}
}
// Sub-header for the expanded track list.
// The item's OWN columns (Item/Type/Source/
// Quality/Tracks/Year) name the collection
// rows, not these child tracks — so a small,
// muted, column-aligned label band sits over
// the inline tracks instead. Reuses the
// shared TrackListHeader with the SAME arms
// the inline TrackRow sets (menu only), so
// "#", "Title", "Duration", "Quality" land on
// their columns by construction. Shown only
// once the tracks have loaded (never over the
// spinner or the empty state).
Item {
visible: !rowCell.expandLoading
&& rowCell.tracksLoaded
&& rowCell.inlineTracks.length > 0
width: parent.width
height: visible ? 22 : 0
TrackListHeader {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
showArtwork: false
showSource: false
showAlbum: false
showFavorite: false
showDownload: false
showMenu: true
showReorder: false
}
// Discreet hairline under the labels.
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: theme.surfaceElevated
}
}
Repeater {
model: rowCell.inlineTracks
delegate: TrackRow {
@@ -1265,6 +1311,15 @@ Rectangle {
m.push({ "label": root.kind === "mixtape"
? t("Convert to Collection", r) : t("Convert to Mixtape", r),
"icon": "cassette-tape", "action": "convert" })
// Sidebar visibility — only meaningful with the collapsible tree on.
// The label reflects the CURRENT state (the mechanism playlists carry).
if (root.collapsibleMyqbzOn) {
m.push({ "sep": true })
m.push({ "label": root.doc.hidden === true
? t("Show in sidebar", r) : t("Hide from sidebar", r),
"icon": root.doc.hidden === true ? "eye" : "eye-off",
"action": "toggle-hidden" })
}
m.push({ "sep": true })
m.push({ "label": t("Delete", r), "icon": "trash-2", "action": "delete", "danger": true })
return m
@@ -1277,6 +1332,8 @@ Rectangle {
else if (a === "clear-cover") QbzMyQbz.removeCover()
else if (a === "play-mode") QbzMyQbz.togglePlayMode()
else if (a === "convert") QbzMyQbz.convertKind()
else if (a === "toggle-hidden")
QbzMyQbz.setCollectionHidden(root.doc.id, root.doc.hidden !== true)
else if (a === "delete") QbzMyQbz.openDelete()
}