Files
qbz/crates/qbz-qt/qml/views/local/LocalAlbumHeader.qml
T
vicrodh d244213a64 fix(library): harden cross-source catalog data
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic.

Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version.

cx
2026-08-25 18:26:06 -06:00

212 lines
7.6 KiB
QML

// Local album header block (album/LocalAlbumView.slint:213-404): the 224px
// cover, the title / artist line with the "+N more artists" expander and its
// expanded list, the info line, the local action row (play / shuffle / edit
// tags / add to playlist / add to Mixtape) and the version picker.
//
// `artistsExpanded` is view-local and resets when another album opens — the
// Slint watches its own mirror property for exactly this (:156).
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Row {
id: root
property var album: null
/// De-duplicated contributor list (>1 turns the expander on).
property var allArtists: []
property string infoLine: ""
property var versions: []
property string coverSource: ""
/// Per-item cover state, owned by the page (LocalAlbumView).
/// `coverPending` means "this album EXPECTS a cover" — whether one has
/// arrived is decided here, by the image's own readiness, never by the
/// path being non-empty (see theme/RoundedImage.qml's contract).
property bool coverPending: false
/// Optional: true while the page's artwork pass is still in flight. It
/// suspends the settle countdown so a slow cold decode cannot expire the
/// placeholder a beat before the cover lands. Defaults to false, so the
/// page need not supply it.
property bool artPassActive: false
property bool skelPhase: false
property int artSettleMs: 0
signal openArtist(string name)
QbzTheme { id: theme }
property bool artistsExpanded: false
onAlbumChanged: artistsExpanded = false
spacing: 32
Rectangle {
width: 224
height: 224
radius: 12
color: theme.surfaceElevated
clip: true
RoundedImage {
id: headerArt
anchors.fill: parent
source: root.coverSource
radius: 12
}
// Per-item: hands over on the paint, not the path; settles out when
// the album has none (local artwork drops keys with no cover).
QbzSkeleton {
variant: "art"
anchors.fill: parent
blockRadius: 12
pending: root.coverPending
coverReady: headerArt.ready
phase: root.skelPhase
settleMs: root.artSettleMs
settleHold: root.artPassActive
}
}
Column {
width: parent.width - 224 - 32
topPadding: 4
spacing: 0
Text {
width: parent.width
text: root.album ? (root.album.title || "") : ""
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
Item { width: 1; height: 4 }
Row {
spacing: 8
Text {
text: root.album ? (root.album.artist || "") : ""
color: artistArea.containsMouse ? theme.textPrimary : theme.textSecondary
font.pixelSize: theme.fontHeading
font.weight: theme.weightBold
MouseArea {
id: artistArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openArtist(root.album.artist)
}
}
// Multi-artist album (compilations): "+N more artists" expands
// the full track-artist list below.
Text {
visible: root.allArtists.length > 1
anchors.verticalCenter: parent.verticalCenter
text: root.artistsExpanded
? QbzSession.tr("Show less", QbzSession.trRev)
: "+" + (root.allArtists.length - 1) + " "
+ QbzSession.tr("more artists", QbzSession.trRev)
color: moreArea.containsMouse ? theme.accent : theme.textMuted
font.pixelSize: theme.fontBody
MouseArea {
id: moreArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.artistsExpanded = !root.artistsExpanded
}
}
}
// Expanded track-artist list — each entry routes to the Local
// Library Artists tab (name-based).
Column {
visible: root.artistsExpanded && root.allArtists.length > 1
width: parent.width
topPadding: 6
spacing: 0
Repeater {
model: root.allArtists
delegate: Text {
id: nameRow
required property string modelData
height: 24
text: modelData
color: nameArea.containsMouse ? theme.accent : theme.textSecondary
font.pixelSize: theme.fontBody
verticalAlignment: Text.AlignVCenter
MouseArea {
id: nameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openArtist(nameRow.modelData)
}
}
}
}
Item { width: 1; height: 10 }
Text {
width: parent.width
text: root.infoLine
color: theme.textSecondary
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
Item { width: 1; height: 20 }
// ---- Local action row ----
Row {
spacing: 12
QbzCircleAction {
name: "play-fill"
primary: true
onClicked: QbzLocal.albumSelectedAction("play", "")
}
QbzCircleAction {
name: "shuffle"
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.albumSelectedAction("shuffle", "")
}
// ASSET GAP: Slint uses `pencil`; the Qt set ships pen-line.
QbzCircleAction {
name: "pen-line"
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.albumEditTags(root.album.id)
}
QbzCircleAction {
name: "list-plus"
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.albumAddToPlaylist(root.album.id)
}
QbzCircleAction {
name: "cassette-tape"
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzLocal.albumAddToMixtape(root.album.id)
}
}
// ---- Version picker (multiple physical copies only) ----
Item {
visible: root.versions.length > 1
width: 1
height: visible ? 16 : 0
}
Row {
visible: root.versions.length > 1
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.versions.length + " "
+ QbzSession.tr("versions", QbzSession.trRev) + ":"
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
VersionPicker {
anchors.verticalCenter: parent.verticalCenter
versions: root.versions
current: root.album ? (root.album.versionIndex || 0) : 0
onPicked: function (i) { QbzLocal.albumSelectVersion(i) }
}
}
}
}