Files
qbz/crates/qbz-qt/qml/controls/QbzJumpNavBar.qml
T
vicrodh 9ab3d7db96 fix(qbz-qt): local albums route to the local view, and the detail-page parity round
**The empty album view.** Opening a local album from Library "All" landed on a
blank page. The mixed feed hands Qobuz and local albums to the SAME card, and
the card always called the catalog open — so a local id, which is a group key
or a filesystem path, went to /album/get and came back 404. It was in the
owner's log before this round as `get_album(/mnt/nas/…) returned 404`; what
changed is that clearing the stale document turned "shows the previous album"
into "shows nothing", which is how it surfaced. Routing now happens in
open_album itself, using the predicate the feed already uses to route
favourites, so every call site is covered at once instead of card by card.

**Header atmosphere.** Both detail pages get Slint's two-band gradient, tinted
from THIS page's artwork through the ported header_tint — not the now-playing
ambient triad, which is a different subject with a different algorithm.

**Artist page.** The catalog/library toggle existed but was broken: a
hand-rolled copy of the shared tab bar whose delegate walked the wrong parent
chain, so it threw on every evaluation and never took its active colours. It
now mounts the shared control. Added the JUMP TO label, the tab-strip search
with the client-side filter it drives, and the year under release-section
albums. The Network panel ran the full window height and never auto-opened; it
is bounded below the tab strip now and opens unless the content is constrained,
as Slint does. The action circles were the wrong size with the wrong glyph
colour and no overlay arm — which would have left them dark-on-dark once the
gradient landed.

**Quality in track rows.** The column rendered a bare tier; it carries the
badge now, in the one shared row component every surface mounts. playlist_qt
was the only producer not emitting the bit-depth line.

**Skeletons hand over instead of disappearing.** They cleared when the PATH
arrived, but the image still had to load and rasterize after that, so the
placeholder vanished and left an empty cell. They now wait for the image to be
ready.

**Local covers.** Most local covers ARE already the scanner's thumbnail, but
re-hashing their path produced a different filename, so each one was decoded,
resized to its own size and re-encoded into a duplicate before being shown.
That was most of the wait.

cc
2026-07-28 19:01:25 -06:00

162 lines
6.1 KiB
QML

// QbzJumpNavBar — 1:1 port of primitives/JumpNavBar.slint.
//
// The section-jump strip that sits between a page header and its body:
// [JUMP TO] tab tab tab … [magnifier]
// The Slint mounts it on the artist page and (without the search arm) on the
// label page, which is why it lives in controls/ rather than inside a view.
//
// Numbers, read off primitives/JumpNavBar.slint:
// :59 height 44 :60 background = `bar-bg` (host passes the page fill)
// :65 1px border-subtle hairline pinned to the BOTTOM edge
// :74 padding-left/right 32 :76 spacing 18 between label / tabs / search
// :84 "JUMP TO" — legal (13px), semibold, text-muted, letter-spacing 0.5
// :106 tab spacing 14; :111 32px tall cell, width = text + 4, text x = 2
// :116 active -> text-primary + semibold, hover -> text-primary,
// else text-secondary + regular; :128 2px accent underline when active
// :157 open search field 216px wide, 200ms ease-in-out, surface-card
// :260 closed toggle 32px, radius 6, hover surface-elevated, 16px glyph
//
// The search affordance is the SHARED controls/QbzLineEdit expandable arm
// (ExpandableSearch.slint), not a second inline input: same interaction —
// closed magnifier slot, opens right-anchored growing LEFT over the tabs,
// X clears and closes, Esc closes. Its closed slot is 34px against the
// .slint's 32px; that 2px is the price of not owning a fourth search box.
//
// Presentation only: sticky positioning and what a tab scrolls to are the
// host's business (the .slint says the same at :12).
import QtQuick
import com.blitzfc.qbz
import "../theme"
Rectangle {
id: root
QbzTheme { id: theme }
/// [{ id, label }] — the sections present on the page.
property var tabs: []
property string activeTabId: ""
/// Page fill, so the bar reads as part of the page (and can go
/// translucent under the dynamic background, like the .slint's bar-bg).
property color barBg: theme.surfaceMain
/// The label page mounts the bar WITHOUT the search affordance.
property bool showSearch: true
/// Horizontal inset. 32 = the .slint's own padding for a full-bleed
/// mount; a host that already pads its column passes 0.
property real padH: 32
signal tabClicked(string id)
signal searchEdited(string text)
/// Host hook: clear the filter when the field closes itself (Esc / X).
property alias searchOpen: searchBox.open
height: 44
color: root.barBg
Rectangle {
x: 0
y: parent.height - 1
width: parent.width
height: 1
color: theme.borderSubtle
}
Text {
id: jumpLabel
x: root.padH
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("JUMP TO", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.weight: theme.weightSemibold
font.letterSpacing: 0.5
}
// Tab strip. Clipped so a long discography's tabs never spill under the
// search slot (the .slint's `center-region { clip: true }`).
Item {
id: centerRegion
x: jumpLabel.x + jumpLabel.width + 18
y: 0
width: Math.max(0, root.width - root.padH
- (searchSlot.visible ? searchSlot.width + 18 : 0)
- x)
height: parent.height
clip: true
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 14
Repeater {
model: root.tabs
delegate: Item {
id: tabCell
required property var modelData
readonly property bool active: modelData.id === root.activeTabId
width: tabText.implicitWidth + 4
height: 32
Text {
id: tabText
x: 2
height: parent.height
verticalAlignment: Text.AlignVCenter
text: tabCell.modelData.label
color: (tabCell.active || tabArea.containsMouse)
? theme.textPrimary : theme.textSecondary
font.pixelSize: theme.fontBody
font.weight: tabCell.active ? theme.weightSemibold
: theme.weightRegular
}
Rectangle {
visible: tabCell.active
x: 0
y: parent.height - 2
width: parent.width
height: 2
color: theme.accent
}
MouseArea {
id: tabArea
anchors.fill: parent
// The .slint disables the tabs while the field is
// open (they are behind it).
enabled: !searchBox.open
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.tabClicked(tabCell.modelData.id)
}
}
}
}
}
// Search slot — keeps the CLOSED footprint; the field itself grows left
// over the tabs, so nothing in the bar reflows when it opens.
Item {
id: searchSlot
visible: root.showSearch
width: searchBox.width
height: searchBox.height
anchors.right: parent.right
anchors.rightMargin: root.padH
anchors.verticalCenter: parent.verticalCenter
QbzLineEdit {
id: searchBox
searchMode: true
expandable: true
// surface-card overlay fill (JumpNavBar.slint:164), not the
// toolbar's elevated.
elevated: false
openWidth: Math.min(216, Math.max(120, centerRegion.width))
placeholder: QbzSession.tr("Search this page…", QbzSession.trRev)
onEdited: function (v) { root.searchEdited(v) }
}
}
}