Files
qbz/crates/qbz-qt/qml/views/local/LocalEphemeralPane.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

245 lines
11 KiB
QML

// Ephemeral-folder pane (LocalLibraryView.slint:114 EphemeralPane) — shown
// in the Folders tab while an ad-hoc folder is open. Album-grouped (a folder
// can hold several albums): each block is a 56px cover + title/artist/meta,
// with a per-album play button ONLY for multi-album sessions, then the
// block's tracks.
//
// Metadata-bound actions (favorite / playlist / album+artist links) are off:
// ephemeral tracks have no DB row. The CUE badge marks a single-file rip.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../rows"
import "../../theme"
Item {
id: root
property var view: null
QbzTheme { id: theme }
readonly property var eph: view ? view.ephemeral : null
readonly property var albums: eph && eph.albums ? eph.albums : []
Flickable {
id: pane
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: page.height + 100
boundsBehavior: Flickable.StopAtBounds
Column {
id: page
x: 32
y: 16
width: parent.width - 64
spacing: 14
// ---- Header: name + count · path, Play all, Clear ----
Row {
width: parent.width
spacing: 12
Column {
width: parent.width - 2 * 40 - 2 * 12
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: root.eph ? (root.eph.name || "") : ""
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
Text {
width: parent.width
text: (root.eph ? (root.eph.trackCount || 0) : 0) + " "
+ QbzSession.tr("tracks", QbzSession.trRev) + " · "
+ (root.eph ? (root.eph.path || "") : "")
color: theme.textMuted
font.pixelSize: theme.fontLegal
elide: Text.ElideRight
}
}
QbzCircleAction {
name: "play-fill"
primary: true
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.ephemeralPlayAll()
}
QbzCircleAction {
name: "x"
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.ephemeralClear()
}
}
// Scanning — the shape of the album blocks that are coming
// (56px cover + two text bars per block), not a spinner.
QbzSkeleton {
visible: QbzLocal.localEphemeralLoading
variant: "rowList"
width: parent.width
height: visible ? 3 * 68 : 0
rowH: 56
rowGap: 12
rowArtSize: 56
phase: root.view ? root.view.skelPhase : false
}
// Empty (no playable tracks).
Text {
visible: !QbzLocal.localEphemeralLoading && root.albums.length === 0
width: parent.width
topPadding: 24
text: QbzSession.tr("No playable tracks in this folder.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontBody
horizontalAlignment: Text.AlignHCenter
}
// ---- Album blocks ----
Column {
visible: !QbzLocal.localEphemeralLoading
width: parent.width
spacing: 22
Repeater {
model: root.albums
delegate: Column {
id: block
required property var modelData
width: page.width
spacing: 6
Row {
width: parent.width
spacing: 12
Rectangle {
width: 56
height: 56
radius: 6
color: theme.surfaceHover
clip: true
QbzIcon {
name: "disc-3"
width: 24
height: 24
anchors.centerIn: parent
tintName: "muted"
}
RoundedImage {
id: blockArt
anchors.fill: parent
source: root.view
? root.view.artPathOf(block.modelData.artKey) : ""
radius: 6
}
// Per-item: hands over when THIS album's
// cover is actually painted, settles out when
// it has none.
QbzSkeleton {
variant: "art"
anchors.fill: parent
blockRadius: 6
pending: root.view
? root.view.artWanted(block.modelData.artKey) : false
coverReady: blockArt.ready
phase: root.view ? root.view.skelPhase : false
settleMs: root.view ? root.view.artSettleMs : 0
settleHold: root.view ? root.view.artPulse : false
}
}
Column {
width: parent.width - 56 - 12
- (root.eph && root.eph.multiAlbum ? 46 : 0)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Row {
width: parent.width
spacing: 6
Text {
width: parent.width - (block.modelData.isCue ? 44 : 0)
text: block.modelData.title || ""
color: theme.textPrimary
font.pixelSize: theme.fontBody
font.weight: theme.weightSemibold
elide: Text.ElideRight
}
// CUE badge (single-file rip) — a 16px
// rounded rect, not a pill (ADR-008).
Rectangle {
visible: block.modelData.isCue === true
anchors.verticalCenter: parent.verticalCenter
width: cueText.implicitWidth + 12
height: 16
radius: 3
color: theme.surfaceElevated
Text {
id: cueText
anchors.centerIn: parent
text: "CUE"
color: theme.textSecondary
font.pixelSize: theme.fontLegal
font.weight: theme.weightSemibold
}
}
}
Text {
width: parent.width
text: block.modelData.artist || ""
color: theme.textSecondary
font.pixelSize: theme.fontLegal
elide: Text.ElideRight
}
Text {
visible: (block.modelData.meta || "") !== ""
width: parent.width
text: block.modelData.meta || ""
color: theme.textMuted
font.pixelSize: theme.fontLegal
elide: Text.ElideRight
}
}
QbzCircleAction {
visible: root.eph && root.eph.multiAlbum === true
anchors.verticalCenter: parent.verticalCenter
name: "play-fill"
onClicked: QbzLocal.ephemeralPlayAlbum(block.modelData.groupKey)
}
}
Repeater {
model: block.modelData.tracks || []
delegate: TrackRow {
required property var modelData
required property int index
width: page.width
item: modelData
number: index + 1
showArtwork: false
showAlbum: false
showFavorite: false
showDownload: false
showMenu: false
draggable: false
onPlayRequested: QbzLocal.ephemeralPlayTrack(modelData.id)
}
}
}
}
}
}
}
QbzScrollBar {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.top: parent.top
anchors.bottom: parent.bottom
target: pane
visible: pane.contentHeight > pane.height
}
}