Ports the whole playlist-organisation domain, which had no counterpart in this frontend, plus the Library work the owner asked not to defer. Playlist Manager. New controller (playlist_manager_qt, _rows, _ops) over the already-ported folders_qt repository, which until now had zero consumers. Grid, list and folder-tree modes; sort, filter and search; folder cards and chips; drag reorder and up/down arrows. Reached from the sidebar's "..." menu, whose New folder / Manage playlists / Import rows had been rendering dimmed and inert behind a comment saying the seam did not exist. Folders. QbzFolderEdit plus the create/edit modals and PmFolderIcon: preset or custom-image icons, colour, hidden flag, delete. Deleting a folder nulls its LOCAL members' folder_id explicitly, because the app keeps the foreign_keys pragma off, so the schema's ON DELETE SET NULL never fires for them. QbzPlaylistEdit. One shared rename/description/delete editor replacing the inline popup in PlaylistView. Three corrections the old path had wrong: the description is carried through a rename instead of being written as NULL, a FOLLOWED playlist routes to unsubscribe (playlist/delete returns 200 and silently no-ops on a playlist you do not own), and the post-delete back navigation is conditional on actually being on that playlist's page, so deleting from the manager no longer navigates you off it. Importer. Spotify / Apple Music / Tidal / Deezer, over the shared qbz-playlist-import crate. Picker local mode. The reference's four-case matrix — target local or Qobuz, payload local refs or Qobuz ids. Local playlists list unconditionally and Qobuz ones only when online. Local refs onto a Qobuz playlist write the library.db sidecar at next_playlist_sidecar_position(pid, qobuz_count); the 0-based enumerate the reference used before collided slots and lost rows. The carried payload can no longer be parsed as a catalog id when it is a library row id. Unblocks the local bulk add-to-playlist. Library. A-Z jump strip, Albums list mode, Artists side panel, per-tab persistence and multi-select. Preferences land in the same <data-dir>/qbz/favorites_ui.json the shipping build writes, so switching between the two frontends keeps them. LibraryView is split into views/library/ on the way past 1800 lines. Local library. Group-by now persists; playback queues the order on screen rather than the SQL page order; switching album-identity mode invalidates the Artists tab; artist matching folds diacritics and compares normalized credit parts, so "Air" stops listing "Airbourne" and "Beyonce" stops being two artists. Offline. activate_offline never persisted a last-user marker, so every per-user path resolved to None and all local reads came back empty; a session that started offline saw no local playlists at all. The sidebar's load-once latch was function-local and therefore unreachable from the logout reset, so a second login never rebuilt the tree. Both are session-scoped now, and the sidebar Refresh row runs the local-safe verb instead of being dimmed with no way to recover. Also fixes, found while verifying rather than reported: artwork derivatives were written straight to their final path, so a QML Image resolving one mid-encode opened a truncated png and Qt cached that failure against the URL for the rest of the session; Library's track group separators referenced a Component declared in another delegate's scope and could never render; the genre filter was drawn on Tracks and Albums but applied only on All; an All-feed row called a player method that does not exist; and the sidebar's drop-hot highlight used Slint's #RRGGBBAA literals in Qt's #AARRGGBB slot. 17 new msgids across all 8 locales. cc
672 lines
29 KiB
QML
672 lines
29 KiB
QML
// Label landing page — QML port of label/LabelPageView.slint.
|
|
//
|
|
// A circular-portrait header with description + Follow, a JUMP TO strip, a
|
|
// Popular Tracks list with a 5 -> 20 -> 50 reveal, and the Releases /
|
|
// Critics' Picks / Playlists / Artists / More Labels carousels.
|
|
//
|
|
// SCROLLING-PAGE CONVENTION (AlbumView / ArtistView / PlaylistView): the
|
|
// whole page scrolls in one Flickable whose Column pads 32 / 32 / top 11 /
|
|
// bottom 100. The .slint's NavButtons row is dropped (back/forward live in
|
|
// the shell header, HeaderBar.qml:235) but its 22px spacer is KEPT, so the
|
|
// header sits where every other detail page's does.
|
|
//
|
|
// POC-NOTEs, each deliberate:
|
|
// - JUMP TO is NOT sticky. ArtistView.qml:1204 documents the same call for
|
|
// the .slint's sticky clamp; matching it keeps the two detail pages
|
|
// consistent. This is the one accepted visual delta.
|
|
// - NO multi-select — and the ORIGINAL reason is now STALE on both halves:
|
|
// rows/TrackRow.qml:116 DOES have the `selectMode` arm, and add-to-playlist
|
|
// DOES have a bridge seam (QbzPlaylistPicker.openForTracks,
|
|
// src/playlist_picker_bridge.rs). What is still missing is only the
|
|
// selection STATE and the bar itself (LibraryView.qml's `tracksSelected`
|
|
// map is the pattern to copy). Until then the select disc renders DIMMED
|
|
// and inert (the ArtistView "Radio" precedent) and the bar is absent.
|
|
// Per-ROW add-to-playlist and add-to-mixtape are both LIVE on this page
|
|
// already, through the shared TrackRow menu.
|
|
// - NO "In library" tab. The Qt library feed carries no label id, so
|
|
// `libraryCount` is always 0 and the SegmentedTabBar never mounts (see
|
|
// src/label_qt.rs). A toggle that switches to an empty tab is worse.
|
|
// - Share copies the Qobuz link with no toast: there is no Rust clipboard
|
|
// bridge and no toast seam (TrackRow.qml:538-558 documents both).
|
|
// - No scroll restore (the port has no NavState.restore-scope counterpart).
|
|
//
|
|
// LOGO FIT — a CONSCIOUS deviation: both .slint label views draw the logo
|
|
// `image-fit: cover` (LabelPageView.slint:116). This port draws it CONTAIN,
|
|
// the way qml/cards/LabelCard.qml already does, because label logos are
|
|
// wordmarks on transparent backgrounds and cropping one to a circle cuts the
|
|
// name off. The Slint behaviour is cover; this is not it, on purpose.
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
import "../cards"
|
|
import "../controls"
|
|
import "../rows"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: QbzShell.ambientMode > 0 && QbzPlayer.npHasTrack
|
|
radius: 12
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property var doc: {
|
|
try {
|
|
return JSON.parse(QbzHome.labelJson)
|
|
} catch (e) {
|
|
return {}
|
|
}
|
|
}
|
|
readonly property var topTracks: doc.topTracks || []
|
|
readonly property var releases: doc.releases || []
|
|
readonly property var critics: doc.critics || []
|
|
readonly property var playlists: doc.playlists || []
|
|
readonly property var artists: doc.artists || []
|
|
readonly property var moreLabels: doc.moreLabels || []
|
|
readonly property var jumpTabs: doc.jumpTabs || []
|
|
|
|
property string activeJumpTab: "about"
|
|
/// Popular Tracks progressive reveal: 5 -> 20 -> 50 (the Tauri steps).
|
|
property int previewCount: 5
|
|
property string loadedLabelId: ""
|
|
|
|
// Reset the per-label view state when the id actually changes (the
|
|
// document is republished on the artwork pass too — ArtistView's
|
|
// syncArtistState rule).
|
|
onDocChanged: {
|
|
var id = doc.id || ""
|
|
if (id === root.loadedLabelId)
|
|
return
|
|
root.loadedLabelId = id
|
|
root.previewCount = 5
|
|
root.activeJumpTab = "about"
|
|
flick.contentY = 0
|
|
}
|
|
|
|
function scrollToSection(id) {
|
|
root.activeJumpTab = id
|
|
if (id === "about") {
|
|
flick.contentY = 0
|
|
return
|
|
}
|
|
for (var i = 0; i < catalog.children.length; i++) {
|
|
var c = catalog.children[i]
|
|
if (c.anchorId !== undefined && c.anchorId === id) {
|
|
var p = c.mapToItem(flick.contentItem, 0, 0)
|
|
flick.contentY = Math.max(0, p.y - 48)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Share (TrackRow.qml's Loader/TextEdit pattern — QtQuick exposes no
|
|
// Clipboard type, and there is no toast seam, so the copy is silent).
|
|
Loader {
|
|
id: clipLoader
|
|
active: false
|
|
sourceComponent: TextEdit { visible: false }
|
|
}
|
|
function copyToClipboard(text) {
|
|
if (!text || text === "")
|
|
return
|
|
clipLoader.active = true
|
|
clipLoader.item.text = text
|
|
clipLoader.item.selectAll()
|
|
clipLoader.item.copy()
|
|
clipLoader.active = false
|
|
}
|
|
|
|
// ============================ shared rail =============================
|
|
// ONE horizontal rail for all five carousels; `kind` picks the card, the
|
|
// way the .slint mounts Carousel / PlaylistCarousel / ArtistCarousel.
|
|
component CardRail: Column {
|
|
id: cr
|
|
property string title: ""
|
|
property var items: []
|
|
/// "album" | "playlist" | "artist" | "label"
|
|
property string kind: "album"
|
|
property bool showViewAll: false
|
|
signal viewAllClicked()
|
|
|
|
width: parent ? parent.width : 0
|
|
spacing: 12
|
|
|
|
readonly property int perPage: Math.max(1, Math.floor((crList.width + 32) / 232))
|
|
readonly property int step: perPage * 232
|
|
readonly property real maxScroll: Math.max(0, crList.contentWidth - crList.width)
|
|
|
|
QbzSectionHeader {
|
|
title: cr.title
|
|
leftEnabled: crList.contentX > 1
|
|
rightEnabled: crList.contentX < cr.maxScroll - 1
|
|
showViewAll: cr.showViewAll
|
|
onViewAllClicked: cr.viewAllClicked()
|
|
onPageLeft: crList.contentX = Math.max(0, crList.contentX - cr.step)
|
|
onPageRight: crList.contentX = Math.min(cr.maxScroll, crList.contentX + cr.step)
|
|
}
|
|
Item {
|
|
width: parent.width
|
|
height: 246
|
|
ListView {
|
|
id: crList
|
|
anchors.fill: parent
|
|
orientation: ListView.Horizontal
|
|
spacing: 32
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
model: cr.items
|
|
delegate: Item {
|
|
id: crCell
|
|
required property var modelData
|
|
width: 200
|
|
height: 246
|
|
|
|
Component {
|
|
id: crAlbum
|
|
AlbumCard {
|
|
albumId: crCell.modelData.id
|
|
title: crCell.modelData.title
|
|
artist: crCell.modelData.artist
|
|
artistId: crCell.modelData.artistId
|
|
genre: crCell.modelData.genre
|
|
year: crCell.modelData.year
|
|
qualityTier: crCell.modelData.qualityTier
|
|
ribbon: crCell.modelData.ribbon || ""
|
|
ribbonKind: crCell.modelData.ribbonKind || ""
|
|
artSource: crCell.modelData.artPath || ""
|
|
isPinned: crCell.modelData.isPinned === true
|
|
// The pin payload's display snapshot — the REMOTE
|
|
// url (artPath is the local cache path). Without
|
|
// it a label release pinned from here landed in
|
|
// the Home Pinned rail as a grey placeholder.
|
|
artworkUrl: crCell.modelData.artUrl || ""
|
|
// Stamped on the row by label_qt; false made the
|
|
// glyph lie and inverted the first click.
|
|
isFavorite: crCell.modelData.isFavorite === true
|
|
}
|
|
}
|
|
Component {
|
|
id: crPlaylist
|
|
PlaylistCard {
|
|
// artworkUrl defaults to `item.artUrl` (the card's
|
|
// own arm), so only the pin state is handed over.
|
|
item: crCell.modelData
|
|
artSource: crCell.modelData.artPath || ""
|
|
isPinned: crCell.modelData.isPinned === true
|
|
}
|
|
}
|
|
Component {
|
|
id: crArtist
|
|
ArtistCard {
|
|
item: crCell.modelData
|
|
artSource: crCell.modelData.artPath || ""
|
|
isPinned: crCell.modelData.isPinned === true
|
|
artworkUrl: crCell.modelData.artUrl || ""
|
|
// LabelPageView.slint:524 passes card-follow-mode
|
|
// "none" here — and now that ArtistCard's follow
|
|
// arm is LIVE (its `hasFollowSeam: false` was a
|
|
// factual error: library_toggle_favorite routes
|
|
// "artist"), this line is what actually keeps the
|
|
// chip off this carousel instead of a constant.
|
|
followMode: "none"
|
|
}
|
|
}
|
|
Component {
|
|
id: crLabel
|
|
LabelCard {
|
|
item: crCell.modelData
|
|
artSource: crCell.modelData.artPath || ""
|
|
}
|
|
}
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: cr.kind === "playlist" ? crPlaylist
|
|
: cr.kind === "artist" ? crArtist
|
|
: cr.kind === "label" ? crLabel : crAlbum
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================ offline gate ============================
|
|
QbzOfflinePlaceholder {
|
|
visible: QbzSession.offline
|
|
anchors.centerIn: parent
|
|
showSettingsAction: true
|
|
onSettingsClicked: QbzShell.navigateTo("settings")
|
|
}
|
|
|
|
// ============================ the page ================================
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
visible: !QbzSession.offline
|
|
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
|
|
|
|
// The .slint's NavButtons row is not drawn (shell header owns
|
|
// back/forward) but its 22px spacer is kept.
|
|
Item { width: 1; height: 22 }
|
|
|
|
// --- Header ---------------------------------------------------
|
|
Row {
|
|
width: parent.width - 64
|
|
spacing: 32
|
|
|
|
Rectangle {
|
|
width: 180
|
|
height: 180
|
|
radius: 90
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
|
|
QbzIcon {
|
|
visible: (root.doc.imageUrl || "") === ""
|
|
name: "disc-3"
|
|
width: 60
|
|
height: 60
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
// Contain-fit inside the square inscribed in the circle
|
|
// (180 / sqrt(2) ~= 127 -> margin 26) — see the header
|
|
// note on the logo-fit deviation.
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
anchors.margins: 26
|
|
source: root.doc.artPath || ""
|
|
radius: 0
|
|
fit: "contain"
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 180 - 32
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 8
|
|
spacing: 0
|
|
|
|
Text {
|
|
text: QbzSession.tr("LABEL", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 10
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 1.5
|
|
}
|
|
Item { width: 1; height: 6 }
|
|
Text {
|
|
width: parent.width
|
|
text: root.doc.name || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontSection
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Item { visible: (root.doc.description || "") !== ""; width: 1; height: 12 }
|
|
Text {
|
|
visible: (root.doc.description || "") !== ""
|
|
// .slint: `width: root.width - 296px`.
|
|
width: Math.max(0, root.width - 296)
|
|
text: root.doc.descriptionShort || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Item { visible: root.doc.descriptionTruncated === true; width: 1; height: 4 }
|
|
Text {
|
|
visible: root.doc.descriptionTruncated === true
|
|
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) return
|
|
// The .slint modal heading is @tr("About"),
|
|
// not the label name.
|
|
shell.openTextModal(
|
|
QbzSession.tr("About", QbzSession.trRev),
|
|
root.doc.description || "")
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: 18 }
|
|
|
|
// Action row — NO Play button (owner call, .slint:171-179:
|
|
// Popular Tracks already carries one and it does the same).
|
|
Row {
|
|
width: parent.width
|
|
spacing: 12
|
|
QbzCircleAction {
|
|
id: followBtn
|
|
readonly property bool following: root.doc.isFollowing === true
|
|
name: following ? "check" : "user-plus"
|
|
active: following
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.labelFollow()
|
|
}
|
|
QbzCircleAction {
|
|
name: "shuffle"
|
|
btnEnabled: root.topTracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.labelShuffle()
|
|
}
|
|
QbzCircleAction {
|
|
id: headerMoreBtn
|
|
name: "ellipsis"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: function (mouse) {
|
|
headerMenu.openAtCursor(headerMoreBtn, mouse.x, mouse.y)
|
|
}
|
|
}
|
|
// Clamped stretch (ArtistView.qml:1161 — an unclamped
|
|
// negative width silently reflows the whole row).
|
|
Item {
|
|
width: Math.max(0, parent.width - 3 * 32 - 3 * 12)
|
|
height: 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CardMenu {
|
|
id: headerMenu
|
|
menuWidth: 224
|
|
entries: [
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next",
|
|
"enabled": root.topTracks.length > 0 },
|
|
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later",
|
|
"enabled": root.topTracks.length > 0 },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue",
|
|
"enabled": root.topTracks.length > 0 },
|
|
{ "label": QbzSession.tr("Share", QbzSession.trRev), "icon": "link", "action": "share" },
|
|
]
|
|
onPicked: function (a) {
|
|
if (a === "share")
|
|
root.copyToClipboard("https://open.qobuz.com/label/" + (root.doc.id || ""))
|
|
else
|
|
QbzHome.labelTopAction(a)
|
|
}
|
|
}
|
|
|
|
// --- JUMP TO ---------------------------------------------------
|
|
// The .slint reserves 52px here for its sticky overlay; this port
|
|
// puts the bar itself in the flow (see the header POC-NOTE).
|
|
Item { visible: root.jumpTabs.length > 1; width: 1; height: visible ? 8 : 0 }
|
|
QbzJumpNavBar {
|
|
visible: root.jumpTabs.length > 1
|
|
width: parent.width - 64
|
|
padH: 0
|
|
showSearch: false
|
|
barBg: root.ambientOn ? "transparent" : theme.surfaceMain
|
|
tabs: root.jumpTabs
|
|
activeTabId: root.activeJumpTab
|
|
onTabClicked: function (id) { root.scrollToSection(id) }
|
|
}
|
|
|
|
// --- Catalog ---------------------------------------------------
|
|
Column {
|
|
id: catalog
|
|
width: parent.width - 64
|
|
spacing: 0
|
|
|
|
// First-paint block.
|
|
Item {
|
|
visible: QbzHome.labelLoading && root.topTracks.length === 0
|
|
width: parent.width
|
|
height: visible ? 280 : 0
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: 18
|
|
QbzSpinner {
|
|
size: 36
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Popular Tracks -------------------------------------
|
|
// The WHOLE block is gated on a non-empty list (.slint:313).
|
|
Column {
|
|
property string anchorId: "popular"
|
|
visible: root.topTracks.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: 12
|
|
Text {
|
|
width: Math.max(0, parent.width - 3 * 12 - 44 - 32 - 32)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Popular Tracks", QbzSession.trRev)
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontHeading
|
|
font.weight: theme.weightSemibold
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzCircleAction {
|
|
primary: true
|
|
name: "play-fill"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.labelPlayTop()
|
|
}
|
|
// Multi-select: DIMMED and inert (see the header note).
|
|
QbzCircleAction {
|
|
name: "square-check-big"
|
|
btnEnabled: false
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
QbzCircleAction {
|
|
id: topMoreBtn
|
|
name: "ellipsis"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: function (mouse) {
|
|
topMenu.openAtCursor(topMoreBtn, mouse.x, mouse.y)
|
|
}
|
|
}
|
|
}
|
|
CardMenu {
|
|
id: topMenu
|
|
menuWidth: 224
|
|
entries: [
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
|
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
|
]
|
|
onPicked: function (a) { QbzHome.labelTopAction(a) }
|
|
}
|
|
|
|
Item { width: 1; height: 10 }
|
|
|
|
Repeater {
|
|
model: root.topTracks
|
|
delegate: TrackRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: parent ? parent.width : 0
|
|
visible: index < root.previewCount
|
|
height: visible ? 50 : 0
|
|
item: modelData
|
|
number: index + 1
|
|
showArtwork: true
|
|
showAlbum: true
|
|
onPlayRequested: QbzHome.labelPlayTrack(modelData.id)
|
|
onEnqueueRequested: function (mode) {
|
|
QbzHome.labelEnqueueTrack(modelData.id, mode)
|
|
}
|
|
// MyQBZ "Add to mixtape" — the HOST builds the
|
|
// AddItem array (TrackRow does not know
|
|
// itemType/source).
|
|
//
|
|
// SOURCE: a label page is a Qobuz catalog surface.
|
|
// These rows are the `/label/page` response's
|
|
// `top_tracks` mapped to track rows
|
|
// (label_qt.rs:280-287) — there is no local label
|
|
// library and no non-Qobuz row can reach this
|
|
// Repeater, so `modelData.id` is a Qobuz catalog
|
|
// id by construction of the document.
|
|
onMixtapeRequested: QbzMyQbzAdd.open(JSON.stringify([{
|
|
"itemType": "track", "source": "qobuz",
|
|
"sourceItemId": modelData.id,
|
|
"title": modelData.title || "",
|
|
"subtitle": modelData.artist || "",
|
|
"artworkUrl": "", "year": null, "trackCount": null
|
|
}]))
|
|
}
|
|
}
|
|
|
|
// Reveal control: 5 -> 20 -> 50, then back to 5.
|
|
Item { visible: root.topTracks.length > 5; width: 1; height: visible ? 4 : 0 }
|
|
Item {
|
|
visible: root.topTracks.length > 5
|
|
width: parent.width
|
|
height: visible ? 28 : 0
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: revealText.implicitWidth + 24
|
|
height: 28
|
|
color: "transparent"
|
|
Text {
|
|
id: revealText
|
|
anchors.centerIn: parent
|
|
text: (root.previewCount < 50
|
|
&& root.topTracks.length > root.previewCount)
|
|
? QbzSession.tr("Load more", QbzSession.trRev)
|
|
: QbzSession.tr("View less", QbzSession.trRev)
|
|
color: revealArea.containsMouse ? theme.textPrimary : theme.textSecondary
|
|
font.pixelSize: 13
|
|
}
|
|
MouseArea {
|
|
id: revealArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (root.previewCount < 50
|
|
&& root.topTracks.length > root.previewCount)
|
|
root.previewCount = root.previewCount === 5 ? 20 : 50
|
|
else
|
|
root.previewCount = 5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Releases ---------------------------------------------
|
|
Column {
|
|
property string anchorId: "releases"
|
|
visible: root.releases.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
Item { width: 1; height: 32 }
|
|
CardRail {
|
|
title: QbzSession.tr("Releases", QbzSession.trRev)
|
|
items: root.releases
|
|
kind: "album"
|
|
showViewAll: true
|
|
onViewAllClicked: QbzHome.labelOpenReleases()
|
|
}
|
|
}
|
|
|
|
// --- Critics' Picks ---------------------------------------
|
|
Column {
|
|
property string anchorId: "critics"
|
|
visible: root.critics.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
Item { width: 1; height: 32 }
|
|
CardRail {
|
|
title: QbzSession.tr("Critics' Picks", QbzSession.trRev)
|
|
items: root.critics
|
|
kind: "album"
|
|
}
|
|
}
|
|
|
|
// --- Playlists ---------------------------------------------
|
|
Column {
|
|
property string anchorId: "playlists"
|
|
visible: root.playlists.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
Item { width: 1; height: 32 }
|
|
CardRail {
|
|
title: QbzSession.tr("Playlists", QbzSession.trRev)
|
|
items: root.playlists
|
|
kind: "playlist"
|
|
}
|
|
}
|
|
|
|
// --- Artists ------------------------------------------------
|
|
Column {
|
|
property string anchorId: "artists"
|
|
visible: root.artists.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
Item { width: 1; height: 32 }
|
|
CardRail {
|
|
title: QbzSession.tr("Artists", QbzSession.trRev)
|
|
items: root.artists
|
|
kind: "artist"
|
|
}
|
|
}
|
|
|
|
// --- More Labels --------------------------------------------
|
|
// NOTE the msgid: the carousel is "More labels" (lowercase l)
|
|
// and the JUMP TO tab is "More Labels" — two distinct msgids
|
|
// in the .slint, kept distinct here.
|
|
Column {
|
|
property string anchorId: "labels"
|
|
visible: root.moreLabels.length > 0
|
|
width: parent.width
|
|
spacing: 0
|
|
Item { width: 1; height: 32 }
|
|
CardRail {
|
|
title: QbzSession.tr("More labels", QbzSession.trRev)
|
|
items: root.moreLabels
|
|
kind: "label"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: flick
|
|
}
|
|
}
|