library_bridge.rs has sat in the tree unreferenced since the phase-23 work stopped: no mod declaration, not in build.rs, zero call sites, and duplicating members that were still live in bridge.rs. It could not have been wired as it stood — its module was named qbz_library, which is ambiguous with the qbz-library crate this binary depends on, so it never compiled. The module is now qbz_library_bridge; the QML type name comes from the QObject, so QML is unaffected. Registering it and deleting the duplicates had to happen together. Wiring it alone would have compiled cleanly and produced two independent Library states — QML reading one object while Rust published to the other — with the view simply frozen and nothing in the log. Moved with it: four properties, five invokables, three signals, the seven library-only ui() hops in main.rs, and thirty-four QML references. The Connections blocks in LibraryView, Sidebar, QueuePanel, AlbumView and PlaylistCollage retarget wholesale because every handler in them is a library signal. ArtistView's does not: it mixes onLibraryArtworkReady with onReleaseSectionReady, so it becomes two blocks — retargeting it whole would have orphaned the releases pager, and QML resolves handlers lazily enough that the discography would just have stopped loading with nothing in the log. bridge.rs is down to 389 lines and five domains. cc
724 lines
34 KiB
QML
724 lines
34 KiB
QML
// Album detail page — QML port of album/AlbumPageView.slint.
|
|
//
|
|
// Header (224px cover, title, credited-artist line, meta with label link,
|
|
// description + Read more, CircleAction row), divider, toolbar (quality
|
|
// badge + track search), column header, track list (Disc/work headers,
|
|
// TrackRow replica with the playing-row pill + number↔play cell + live
|
|
// heart), label/awards sidebar, and the two bottom carousels ("From the
|
|
// same artist", "Listening suggestions").
|
|
//
|
|
// POC-NOTEs: multi-select + bulk bar, offline download column, booklet,
|
|
// external links, custom-cover menu, album-info modal, the header
|
|
// atmosphere (album-header-gradient pref) — out of scope (visible stubs
|
|
// are inert). Text keeps the theme colors (header-light = false here).
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../rows"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
// Transparent while the ambient background is active (phase 14 —
|
|
// HomeView.slint:163: the frosted content panel shows through).
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: QbzShell.ambientMode > 0 && QbzPlayer.npHasTrack
|
|
|
|
// Round to the AppShell content-frame bezel (Radius.md): QML clips
|
|
// are rectangular, so the frame's own rounding never reaches the
|
|
// view — the view's own fill must round instead.
|
|
radius: 12
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// The view's album + url-keyed cover map (artwork pipeline).
|
|
readonly property var album: JSON.parse(QbzBridge.albumJson)
|
|
readonly property var header: album.header || ({})
|
|
readonly property var tracks: album.tracks || []
|
|
readonly property var awards: album.awards || []
|
|
property var coverMap: ({})
|
|
// Client-side track search (AlbumActions.search equivalent).
|
|
property string trackQuery: ""
|
|
|
|
readonly property var visibleTracks: {
|
|
if (trackQuery === "") return tracks
|
|
var q = trackQuery.toLowerCase()
|
|
return tracks.filter(function (t) {
|
|
return t.title.toLowerCase().indexOf(q) >= 0
|
|
})
|
|
}
|
|
|
|
// Disc headers + work headers precede their first row (computed once
|
|
// per track list change, mirroring AlbumState's disc-header-number /
|
|
// work-header model fields).
|
|
function headerFor(i) {
|
|
var t = visibleTracks[i]
|
|
if (!t) return null
|
|
if (i === 0) {
|
|
return { disc: t.disc > 1 || (visibleTracks.length > 0 && visibleTracks[visibleTracks.length - 1].disc > 1) ? t.disc : 0,
|
|
work: t.workHeader }
|
|
}
|
|
var prev = visibleTracks[i - 1]
|
|
var multi = visibleTracks[visibleTracks.length - 1].disc > 1
|
|
return { disc: (multi && t.disc !== prev.disc) ? t.disc : 0,
|
|
work: t.workHeader !== prev.workHeader ? t.workHeader : "" }
|
|
}
|
|
|
|
Connections {
|
|
target: QbzLibrary
|
|
function onLibraryArtworkReady(key, path) {
|
|
var m = root.coverMap
|
|
m[key] = path
|
|
root.coverMap = Object.assign({}, m)
|
|
}
|
|
}
|
|
Component.onCompleted: dispatchCovers()
|
|
onAlbumChanged: dispatchCovers()
|
|
// The derived binding settles AFTER onAlbumChanged fires (stale race) —
|
|
// redispatch when the header itself updates.
|
|
onHeaderChanged: dispatchCovers()
|
|
function dispatchCovers() {
|
|
var urls = []
|
|
if (header && header.artUrl) urls.push(header.artUrl)
|
|
var more = album.moreFromArtist || []
|
|
for (var i = 0; i < more.length; i++) if (more[i].artUrl) urls.push(more[i].artUrl)
|
|
var sug = album.suggestions || []
|
|
for (i = 0; i < sug.length; i++) if (sug[i].artUrl) urls.push(sug[i].artUrl)
|
|
if (urls.length > 0) QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
|
|
}
|
|
|
|
// Ghost CircleAction (secondary, on-surface variant): elevated disc,
|
|
// strong ring, text-primary icon (accent when active).
|
|
|
|
|
|
// Track list row (TrackRow.slint replica: number cell, no artwork,
|
|
// Sidebar label/award card (SidebarCard).
|
|
component SidebarCard: Rectangle {
|
|
property string name: ""
|
|
property color gradA: "#6366f1"
|
|
property color gradB: "#8b5cf6"
|
|
property string iconName: "disc"
|
|
signal clicked()
|
|
width: parent ? parent.width : 0
|
|
height: 48
|
|
radius: theme.radiusSm
|
|
color: scArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.margins: 6
|
|
spacing: 10
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop { position: 0.0; color: gradA }
|
|
GradientStop { position: 1.0; color: gradB }
|
|
}
|
|
QbzIcon {
|
|
name: iconName
|
|
width: 13
|
|
height: 13
|
|
anchors.centerIn: parent
|
|
tintName: "primary"
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width - 38
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: name
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
font.weight: theme.weightMedium
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: scArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: parent.clicked()
|
|
}
|
|
}
|
|
component SidebarHeading: Text {
|
|
color: theme.textMuted
|
|
font.pixelSize: 10
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
// ============================ the page ================================
|
|
Flickable {
|
|
id: pageFlick
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: page
|
|
width: parent.width
|
|
leftPadding: 32
|
|
rightPadding: 32
|
|
topPadding: 11
|
|
bottomPadding: 100
|
|
spacing: 0
|
|
|
|
// NavButtons is a 0px placeholder in the Slint source.
|
|
Item { width: 1; height: 22 }
|
|
|
|
// --- Album header -------------------------------------------
|
|
Row {
|
|
width: parent.width - 64
|
|
spacing: 32
|
|
|
|
Rectangle {
|
|
width: 224
|
|
height: 224
|
|
radius: 12
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
source: root.coverMap[header.artUrl] || ""
|
|
radius: 12
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 224 - 32
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 4
|
|
spacing: 0
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: header.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontSection
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
Item { width: 1; height: 4 }
|
|
// Credited-artist line (links + role suffixes).
|
|
Flow {
|
|
width: parent.width
|
|
spacing: 0
|
|
Repeater {
|
|
model: header.credits || []
|
|
delegate: Row {
|
|
required property var modelData
|
|
required property int index
|
|
spacing: 0
|
|
Text {
|
|
visible: index > 0
|
|
text: " • "
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontHeading
|
|
font.weight: theme.weightBold
|
|
}
|
|
Text {
|
|
text: modelData[0]
|
|
color: creditArea.containsMouse && modelData[1] !== "" ? theme.textPrimary : theme.textSecondary
|
|
font.pixelSize: theme.fontHeading
|
|
font.weight: theme.weightBold
|
|
MouseArea {
|
|
id: creditArea
|
|
anchors.fill: parent
|
|
enabled: modelData[1] !== ""
|
|
hoverEnabled: true
|
|
cursorShape: modelData[1] !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: QbzBridge.openArtist(modelData[1])
|
|
}
|
|
}
|
|
Text {
|
|
visible: modelData[2] !== ""
|
|
text: " (" + modelData[2] + ")"
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontHeading
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Item { width: 1; height: 10 }
|
|
// Meta line (label as a clickable link when navigable).
|
|
Row {
|
|
spacing: 0
|
|
visible: (header.labelId || "") !== "" && (header.label || "") !== ""
|
|
Text {
|
|
visible: (header.metaPre || "") !== ""
|
|
text: (header.metaPre || "") + " • "
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
}
|
|
Text {
|
|
text: header.label || ""
|
|
color: labelArea.containsMouse ? theme.accent : theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
MouseArea {
|
|
id: labelArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
// POC-NOTE: no label view yet.
|
|
}
|
|
}
|
|
Text {
|
|
visible: (header.metaPost || "") !== ""
|
|
text: " • " + (header.metaPost || "")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
}
|
|
}
|
|
Text {
|
|
visible: (header.labelId || "") === "" || (header.label || "") === ""
|
|
width: parent.width
|
|
text: header.infoLine || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Editorial description + Read more.
|
|
Item { visible: (header.description || "") !== ""; width: 1; height: 12 }
|
|
Text {
|
|
visible: (header.description || "") !== ""
|
|
width: parent.width
|
|
text: header.descriptionShort || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Item { visible: (header.description || "") !== (header.descriptionShort || ""); width: 1; height: 4 }
|
|
Text {
|
|
visible: (header.description || "") !== (header.descriptionShort || "")
|
|
text: QbzSession.tr("Read more", QbzSession.trRev)
|
|
color: readMoreArea.containsMouse ? theme.accentHover : theme.accent
|
|
font.pixelSize: theme.fontLegal
|
|
MouseArea {
|
|
id: readMoreArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
var shell = root.parent
|
|
while (shell && shell.openTextModal === undefined) shell = shell.parent
|
|
if (shell) shell.openTextModal(QbzSession.tr("About this album", QbzSession.trRev), header.description || "")
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: 20 }
|
|
// Action row (on-surface CircleActions).
|
|
Row {
|
|
spacing: 12
|
|
// Play — accent disc, white glyph (on-surface primary).
|
|
Rectangle {
|
|
width: 44
|
|
height: 44
|
|
radius: 22
|
|
color: playHdrArea.containsMouse ? theme.accentHover : theme.accent
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
name: "play-fill"
|
|
width: 19
|
|
height: 19
|
|
tintName: "primary"
|
|
}
|
|
MouseArea {
|
|
id: playHdrArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzPlayer.playAlbum(header.id)
|
|
}
|
|
}
|
|
QbzCircleAction {
|
|
name: "shuffle"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzPlayer.playAlbumShuffled(header.id)
|
|
}
|
|
QbzCircleAction {
|
|
name: header.isFavorite ? "heart-filled" : "heart"
|
|
active: header.isFavorite === true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: {
|
|
header.isFavorite = !header.isFavorite
|
|
QbzLibrary.libraryToggleFavorite("album", header.id)
|
|
}
|
|
}
|
|
// Radio / Mixtape / Info — INERT stubs (POC-NOTE:
|
|
// radio engines, mixtape store, album-info modal).
|
|
QbzCircleAction { name: "radio"; anchors.verticalCenter: parent.verticalCenter }
|
|
QbzCircleAction { name: "cassette-tape"; anchors.verticalCenter: parent.verticalCenter }
|
|
QbzCircleAction { name: "info"; anchors.verticalCenter: parent.verticalCenter }
|
|
QbzCircleAction {
|
|
id: albumMenuBtn
|
|
name: "ellipsis"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: function (mouse) { albumMenu.openAtCursor(albumMenuBtn, mouse.x, mouse.y) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: 20 }
|
|
// Header divider (the atmosphere ends here in Slint).
|
|
Rectangle { width: parent.width - 64; height: 1; color: theme.borderSubtle }
|
|
Item { width: 1; height: 8 }
|
|
|
|
// --- Track list + label/awards sidebar ----------------------
|
|
Row {
|
|
width: parent.width - 64
|
|
spacing: 32
|
|
|
|
Column {
|
|
width: parent.width - ((header.label || "") !== "" || awards.length > 0 ? 232 : 0)
|
|
spacing: 0
|
|
|
|
// Loading.
|
|
Item {
|
|
visible: QbzBridge.albumLoading && tracks.length === 0
|
|
width: parent.width
|
|
height: 280
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: 18
|
|
QbzSpinner { size: 36; anchors.horizontalCenter: parent.horizontalCenter }
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzSession.tr("Loading album…", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toolbar — quality badge + track search (+ inert select).
|
|
Row {
|
|
visible: !QbzBridge.albumLoading
|
|
width: parent.width
|
|
height: 52
|
|
spacing: 16
|
|
Row {
|
|
id: qualityRow
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 7
|
|
Image {
|
|
visible: header.qualityTier === "hires"
|
|
source: "../assets/hi-res.svg"
|
|
width: 42
|
|
height: 28
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
sourceSize: Qt.size(84, 56)
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
Text {
|
|
visible: (header.qualityDetail || "") !== ""
|
|
text: header.qualityDetail || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
Item { width: parent.width - qualityRow.width - 168 - 30 - 3 * 16; height: 1 }
|
|
Rectangle {
|
|
width: 168
|
|
height: 34
|
|
radius: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: theme.surfaceElevated
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
spacing: 7
|
|
QbzIcon {
|
|
name: "search"
|
|
width: 14
|
|
height: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tintName: "muted"
|
|
}
|
|
TextInput {
|
|
width: parent.width - 21
|
|
height: parent.height
|
|
color: theme.textPrimary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
clip: true
|
|
onTextEdited: root.trackQuery = text
|
|
Text {
|
|
visible: parent.text === ""
|
|
anchors.fill: parent
|
|
text: QbzSession.tr("Search tracks...", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 30
|
|
height: 30
|
|
radius: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: selectArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
QbzIcon {
|
|
name: "square-check-big"
|
|
width: 15
|
|
height: 15
|
|
anchors.centerIn: parent
|
|
tintName: selectArea.containsMouse ? "primary" : "secondary"
|
|
}
|
|
MouseArea {
|
|
id: selectArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
// POC-NOTE: multi-select out of scope.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Column header.
|
|
Row {
|
|
visible: !QbzBridge.albumLoading
|
|
width: parent.width
|
|
height: 40
|
|
leftPadding: 12
|
|
rightPadding: 12
|
|
spacing: 16
|
|
Text { text: "#"; width: 32; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; anchors.verticalCenter: parent.verticalCenter }
|
|
Text { text: QbzSession.tr("Title", QbzSession.trRev); width: parent.width - 32 - 80 - 80 - 28 - 28 - 32 - 5 * 16 - 24; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight }
|
|
Text { text: QbzSession.tr("Duration", QbzSession.trRev); width: 80; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; horizontalAlignment: Text.AlignHCenter; anchors.verticalCenter: parent.verticalCenter }
|
|
Text { text: QbzSession.tr("Quality", QbzSession.trRev); width: 80; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; horizontalAlignment: Text.AlignHCenter; anchors.verticalCenter: parent.verticalCenter }
|
|
Item { width: 28; height: 1; QbzIcon { name: "heart"; width: 14; height: 14; anchors.centerIn: parent; tintName: "muted" } }
|
|
Item { width: 28; height: 1; QbzIcon { name: "cloud-download"; width: 14; height: 14; anchors.centerIn: parent; tintName: "muted" } }
|
|
Item { width: 32; height: 1 }
|
|
}
|
|
|
|
// Rows (with Disc / work headers).
|
|
Repeater {
|
|
model: root.visibleTracks
|
|
delegate: Column {
|
|
required property var modelData
|
|
required property int index
|
|
property var hdr: root.headerFor(index)
|
|
width: parent ? parent.width : 0
|
|
|
|
Rectangle {
|
|
visible: hdr && hdr.disc > 0
|
|
width: parent.width
|
|
height: 40
|
|
color: "transparent"
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Disc", QbzSession.trRev) + " " + hdr.disc
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 0.5
|
|
}
|
|
}
|
|
Row {
|
|
visible: hdr && hdr.work !== ""
|
|
width: parent.width
|
|
leftPadding: 12
|
|
rightPadding: 12
|
|
topPadding: 14
|
|
bottomPadding: 4
|
|
spacing: 0
|
|
Text {
|
|
text: hdr.work
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightBold
|
|
}
|
|
Text {
|
|
visible: modelData.workComposerName !== ""
|
|
text: " ("
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightBold
|
|
}
|
|
Text {
|
|
visible: modelData.workComposerName !== ""
|
|
text: modelData.workComposerName
|
|
color: composerArea.containsMouse && modelData.workComposerId !== "" ? theme.textPrimary : theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightBold
|
|
MouseArea {
|
|
id: composerArea
|
|
anchors.fill: parent
|
|
enabled: modelData.workComposerId !== ""
|
|
hoverEnabled: true
|
|
cursorShape: modelData.workComposerId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: QbzBridge.openArtist(modelData.workComposerId)
|
|
}
|
|
}
|
|
Text {
|
|
visible: modelData.workComposerName !== ""
|
|
text: ")"
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightBold
|
|
}
|
|
}
|
|
TrackRow {
|
|
item: modelData
|
|
number: index + 1
|
|
zebra: true
|
|
clickPlays: false
|
|
artistLink: true
|
|
qualityStyle: "text"
|
|
showDownload: true
|
|
downloadGlyph: true
|
|
menuShowLater: false
|
|
menuShowGoTo: false
|
|
onPlayRequested: QbzPlayer.playAlbumFrom(header.id, item.id)
|
|
onEnqueueRequested: function (m) {
|
|
QbzPlayer.enqueueAlbumTrack(header.id, item.id, m === "next" ? "next" : "later")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Label / awards sidebar (200px).
|
|
Column {
|
|
visible: (header.label || "") !== "" || awards.length > 0
|
|
width: 200
|
|
spacing: 24
|
|
|
|
Column {
|
|
visible: (header.label || "") !== ""
|
|
width: parent.width
|
|
spacing: 8
|
|
SidebarHeading { text: QbzSession.tr("LABEL", QbzSession.trRev) }
|
|
SidebarCard {
|
|
name: header.label || ""
|
|
iconName: "disc"
|
|
gradA: "#6366f1"
|
|
gradB: "#8b5cf6"
|
|
// POC-NOTE: no label view yet.
|
|
}
|
|
}
|
|
Column {
|
|
visible: awards.length > 0
|
|
width: parent.width
|
|
spacing: 8
|
|
SidebarHeading { text: QbzSession.tr("AWARDS", QbzSession.trRev) }
|
|
Repeater {
|
|
model: awards
|
|
delegate: SidebarCard {
|
|
required property var modelData
|
|
name: modelData[1]
|
|
iconName: "award"
|
|
gradA: "#b45309"
|
|
gradB: "#eab308"
|
|
// POC-NOTE: no award view yet.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Bottom carousels ---------------------------------------
|
|
Item { visible: (album.moreFromArtist || []).length > 0; width: 1; height: 40 }
|
|
SectionRail {
|
|
visible: (album.moreFromArtist || []).length > 0
|
|
title: QbzSession.tr("From the same artist", QbzSession.trRev)
|
|
items: album.moreFromArtist || []
|
|
coverMap: root.coverMap
|
|
}
|
|
Item { visible: (album.suggestions || []).length > 0; width: 1; height: 40 }
|
|
SectionRail {
|
|
visible: (album.suggestions || []).length > 0
|
|
title: QbzSession.tr("Listening suggestions", QbzSession.trRev)
|
|
items: album.suggestions || []
|
|
coverMap: root.coverMap
|
|
}
|
|
}
|
|
}
|
|
|
|
// Thin auto-hiding scrollbar (ListScrollbar).
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: pageFlick
|
|
}
|
|
|
|
// Album ⋯ menu (AlbumContextMenu subset — card menu + pin).
|
|
QbzContextMenu {
|
|
id: albumMenu
|
|
menuWidth: 196
|
|
Repeater {
|
|
model: [
|
|
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-plus", "action": "next" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
|
{ "label": header.isFavorite ? QbzSession.tr("Remove from Library", QbzSession.trRev) : QbzSession.tr("Add to Library", QbzSession.trRev), "icon": header.isFavorite ? "heart-filled" : "heart", "action": "favorite" },
|
|
{ "label": header.isPinned ? QbzSession.tr("Unpin", QbzSession.trRev) : QbzSession.tr("Pin", QbzSession.trRev), "icon": header.isPinned ? "pin-filled" : "pin", "action": "pin" },
|
|
]
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: parent ? parent.width : 0
|
|
height: 33
|
|
radius: 5
|
|
color: amiArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
spacing: 8
|
|
QbzIcon { name: modelData.icon; width: 15; height: 15; anchors.verticalCenter: parent.verticalCenter; tintName: "secondary" }
|
|
Text {
|
|
height: parent.height
|
|
width: parent.width - 23
|
|
text: modelData.label
|
|
color: theme.textSecondary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: amiArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
albumMenu.close()
|
|
var a = modelData.action
|
|
if (a === "play") QbzPlayer.playAlbum(header.id)
|
|
else if (a === "next") QbzPlayer.enqueueAlbum(header.id, "next")
|
|
else if (a === "queue") QbzPlayer.enqueueAlbum(header.id, "later")
|
|
else if (a === "favorite") {
|
|
header.isFavorite = !header.isFavorite
|
|
QbzLibrary.libraryToggleFavorite("album", header.id)
|
|
} else if (a === "pin") {
|
|
header.isPinned = !header.isPinned
|
|
QbzLibrary.togglePin("album", header.id, header.title, header.artist, header.artUrl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|