feat(qbz-qt): fill the Album and Artist pages from the integrations
The two pages were missing most of what Slint shows. With MusicBrainz, Last.fm and ListenBrainz wired this round, the sections that depend on them can exist: the artist biography and network, relationship rows, listener figures and the credit blocks the Slint pages carry. Nothing is faked. A section whose data an integration cannot supply is absent rather than an empty frame, and a section fed by an integration the user has not connected degrades to absent without a network call — integrations stay strictly opt-in. The relationship rows are non-navigable for now because resolving a musician name to a catalog artist needs a bridge invokable that is not there yet; an informational row is better than a dead click. cc
This commit is contained in:
7 files changed
+1333
-79
No files matched your search
@@ -7,12 +7,18 @@
|
||||
// (Albums / EPs & Singles / Live / … in the official order, sort menu,
|
||||
// per-section Load more paged through the core), Appears On, Playlists,
|
||||
// Other (collapsed), and the 300px Network sidebar (Network/Magazine
|
||||
// tabs, LABELS, SIMILAR ARTISTS).
|
||||
// tabs, ORIGIN, LABELS, SIMILAR ARTISTS, RELATIONSHIPS, YOU MAY ALSO LIKE,
|
||||
// and the Magazine story teasers).
|
||||
//
|
||||
// POC-NOTEs: MusicBrainz sidebar sections (Origin/Relationships/
|
||||
// Discovery), the Magazine tab's stories, blacklist banner, artist Scene,
|
||||
// Share, Create Collection, radio engines (dropdown inert), multi-select,
|
||||
// the sticky behavior of the JUMP TO bar (it scrolls with the page).
|
||||
// The document arrives in passes: the Qobuz page first, then the Magazine
|
||||
// stories, then MusicBrainz Origin -> Relationships -> Discovery (see
|
||||
// artist_qt.rs). Each MB section renders its own "Loading…" line and, when
|
||||
// MusicBrainz is off in Settings or the artist has no confident MB match, is
|
||||
// simply ABSENT — never an error frame, and nothing is requested.
|
||||
//
|
||||
// POC-NOTEs: blacklist banner, artist Scene, Share, Create Collection, radio
|
||||
// engines (dropdown inert), multi-select, the sticky behavior of the JUMP TO
|
||||
// bar (it scrolls with the page).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -42,6 +48,13 @@ Rectangle {
|
||||
readonly property var labels: artist.labels || []
|
||||
readonly property var similarArtists: artist.similarArtists || []
|
||||
readonly property var playlists: artist.playlists || []
|
||||
// MusicBrainz-driven sidebar payload (artist_qt.rs ArtistNetwork). Absent
|
||||
// on the very first frame of a cold document — every read below is
|
||||
// defaulted so a missing member can never throw.
|
||||
readonly property var network: artist.network || ({})
|
||||
readonly property var mbOrigin: network.origin || ({})
|
||||
readonly property var mbRelationships: network.relationships || ({})
|
||||
readonly property var stories: artist.stories || []
|
||||
|
||||
property var coverMap: ({})
|
||||
property string activeJumpTab: "popular-tracks"
|
||||
@@ -52,6 +65,45 @@ Rectangle {
|
||||
property bool networkOpen: false
|
||||
property string netTab: "network"
|
||||
readonly property int preview: 5
|
||||
// Sidebar lists are unbounded upstream (an orchestra can list 150
|
||||
// members): show a slice, expand on demand — the delegates for the rest
|
||||
// are never instantiated.
|
||||
readonly property int sidebarPreview: 12
|
||||
property bool membersExpanded: false
|
||||
property bool groupsExpanded: false
|
||||
property bool collabsExpanded: false
|
||||
// Thumbs-downed discovery rows, by mbid. Session-only: the Slint app
|
||||
// persists these in its `discovery_dismiss` store, which this POC does
|
||||
// not open, so the rejection lasts as long as the process — it is NOT
|
||||
// written anywhere and makes no claim to be.
|
||||
property var dismissedDiscovery: ({})
|
||||
// The artist the view state (tab choice, dismissals) belongs to. Compared
|
||||
// on every republish so a mid-load pass never resets the user's choices.
|
||||
property string loadedArtistId: ""
|
||||
|
||||
// Optimistic heart/pin state. The document is republished several times
|
||||
// per page now (stories, then each MusicBrainz section), and every
|
||||
// republish re-parses `artist` — a toggle written straight onto the parsed
|
||||
// object would silently pop back. Overrides live here instead and win over
|
||||
// whatever the document says, until the artist changes.
|
||||
property var localToggles: ({})
|
||||
function toggleState(key, fallback) {
|
||||
return localToggles[key] !== undefined ? localToggles[key] : fallback === true
|
||||
}
|
||||
function setToggleState(key, value) {
|
||||
var m = localToggles
|
||||
m[key] = value
|
||||
localToggles = Object.assign({}, m)
|
||||
}
|
||||
|
||||
readonly property var discoveryRows: {
|
||||
var out = []
|
||||
var rows = network.discovery || []
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
if (!dismissedDiscovery[rows[i].mbid]) out.push(rows[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// JUMP TO tabs from the present sections (ArtistState.jump-tabs).
|
||||
readonly property var jumpTabs: {
|
||||
@@ -98,17 +150,60 @@ Rectangle {
|
||||
root.artistChanged()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: dispatchCovers()
|
||||
onArtistChanged: dispatchCovers()
|
||||
Component.onCompleted: {
|
||||
syncArtistState()
|
||||
dispatchCovers()
|
||||
}
|
||||
onArtistChanged: {
|
||||
syncArtistState()
|
||||
dispatchCovers()
|
||||
}
|
||||
// Cover dispatch keys off the raw document (artist.artUrl etc.), so
|
||||
// re-fire when the parsed value actually changes (same stale race).
|
||||
onTopTracksChanged: dispatchCovers()
|
||||
onArtistTabChanged: if (artistTab === "library") dispatchLibCovers()
|
||||
|
||||
// The document is republished several times per page (stories, then each
|
||||
// MusicBrainz section). Reset per-artist view state ONLY when the id
|
||||
// actually changed, or an enrichment pass would yank the sidebar tab back
|
||||
// under the user mid-read.
|
||||
function syncArtistState() {
|
||||
var id = artist.id || ""
|
||||
if (id === loadedArtistId)
|
||||
return
|
||||
loadedArtistId = id
|
||||
// Slint opens a fresh artist on Network, or on Magazine when
|
||||
// MusicBrainz is off (an empty Network tab is worse than none).
|
||||
netTab = (artist.network && artist.network.mbAvailable) ? "network" : "magazine"
|
||||
dismissedDiscovery = ({})
|
||||
localToggles = ({})
|
||||
membersExpanded = false
|
||||
groupsExpanded = false
|
||||
collabsExpanded = false
|
||||
dispatchedCovers = ({})
|
||||
}
|
||||
|
||||
function dispatchLibCovers() {
|
||||
var urls = []
|
||||
var items = libraryTab.libItems || []
|
||||
var urls = []
|
||||
for (var i = 0; i < items.length; i++) if (items[i].imageUrl) urls.push(items[i].imageUrl)
|
||||
if (urls.length > 0) QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
|
||||
dispatchArtwork(urls)
|
||||
}
|
||||
|
||||
// Already-requested artwork keys. With the progressive republish the
|
||||
// dispatch runs once per pass, so re-sending the whole (potentially
|
||||
// several-hundred-entry) URL list every time is pure waste — send only
|
||||
// what is new.
|
||||
property var dispatchedCovers: ({})
|
||||
function dispatchArtwork(urls) {
|
||||
var fresh = []
|
||||
for (var i = 0; i < urls.length; i++) {
|
||||
var u = urls[i]
|
||||
if (!u || dispatchedCovers[u]) continue
|
||||
dispatchedCovers[u] = true
|
||||
fresh.push(u)
|
||||
}
|
||||
if (fresh.length > 0) QbzShell.sidebarArtworkWindow(JSON.stringify(fresh))
|
||||
}
|
||||
|
||||
function dispatchCovers() {
|
||||
@@ -119,7 +214,9 @@ Rectangle {
|
||||
for (i = 0; i < releaseSections.length; i++)
|
||||
for (j = 0; j < releaseSections[i].cards.length; j++)
|
||||
if (releaseSections[i].cards[j].artUrl) urls.push(releaseSections[i].cards[j].artUrl)
|
||||
if (urls.length > 0) QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
|
||||
// Magazine story thumbnails ride the same pipeline (arc-cdn URLs).
|
||||
for (i = 0; i < stories.length; i++) if (stories[i].artUrl) urls.push(stories[i].artUrl)
|
||||
dispatchArtwork(urls)
|
||||
}
|
||||
|
||||
function scrollToSection(id) {
|
||||
@@ -293,8 +390,10 @@ Rectangle {
|
||||
font.weight: theme.weightBold
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
// Favorite (live).
|
||||
// Favorite (live). Reads through the override map so the state
|
||||
// survives a document republish (see root.localToggles).
|
||||
Rectangle {
|
||||
property bool favorite: root.toggleState("track:" + row.id, row.isFavorite)
|
||||
width: 28
|
||||
height: 28
|
||||
radius: theme.radiusSm
|
||||
@@ -302,10 +401,10 @@ Rectangle {
|
||||
color: favArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
name: row.isFavorite ? "heart-filled" : "heart"
|
||||
name: parent.favorite ? "heart-filled" : "heart"
|
||||
width: 16
|
||||
height: 16
|
||||
tintName: row.isFavorite ? "favorite" : (favArea.containsMouse ? "primary" : "muted")
|
||||
tintName: parent.favorite ? "favorite" : (favArea.containsMouse ? "primary" : "muted")
|
||||
}
|
||||
MouseArea {
|
||||
id: favArea
|
||||
@@ -313,7 +412,7 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
row.isFavorite = !row.isFavorite
|
||||
root.setToggleState("track:" + row.id, !parent.favorite)
|
||||
QbzLibrary.libraryToggleFavorite("track", row.id)
|
||||
}
|
||||
}
|
||||
@@ -354,10 +453,14 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar link row (SidebarLink).
|
||||
// Sidebar link row (SidebarLink). `navigable` false = informational row:
|
||||
// no pointer cursor, no hover promise it cannot keep (used by the MB
|
||||
// Relationships rows, which have no destination in this port).
|
||||
component SidebarLink: Rectangle {
|
||||
property string label: ""
|
||||
property string iconName: "user"
|
||||
property string tooltip: ""
|
||||
property bool navigable: true
|
||||
signal clicked()
|
||||
width: parent ? parent.width : 0
|
||||
height: 28
|
||||
@@ -388,8 +491,113 @@ Rectangle {
|
||||
id: slArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.clicked()
|
||||
cursorShape: navigable ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (navigable) parent.clicked()
|
||||
ToolTip.visible: containsMouse && tooltip !== ""
|
||||
ToolTip.text: tooltip
|
||||
ToolTip.delay: 400
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar section heading (11px muted caps, letter-spaced).
|
||||
component SidebarSectionHeading: Text {
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
|
||||
// Small 11px muted line — sub-group labels and the "Loading…"/empty
|
||||
// states inside the sidebar sections.
|
||||
component SidebarNote: Text {
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
// One "KEY value" row of the MB Origin block.
|
||||
component OriginRow: Item {
|
||||
property string key: ""
|
||||
property string value: ""
|
||||
// The host section Column carries 14px of left+right padding, and a
|
||||
// QML Positioner does NOT shrink its children for it — a right-aligned
|
||||
// value bound to the bare parent.width would run past the sidebar
|
||||
// edge. Subtract it here (this row only ever lives in that section).
|
||||
width: parent ? parent.width - 28 : 0
|
||||
height: 20
|
||||
Text {
|
||||
id: originKey
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: key
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.left: originKey.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: value
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 13
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// One MB relationship sub-group (Members & Former / Member Of /
|
||||
// Collaborators) with a preview cap + expander.
|
||||
component RelationshipGroup: Column {
|
||||
id: relGroup
|
||||
property string title: ""
|
||||
property var rows: []
|
||||
property string iconName: "user"
|
||||
property bool expanded: false
|
||||
signal toggled()
|
||||
visible: rows.length > 0
|
||||
// -28 = the host section Column's left+right padding (see OriginRow).
|
||||
width: parent ? parent.width - 28 : 0
|
||||
spacing: 2
|
||||
topPadding: 2
|
||||
SidebarNote {
|
||||
text: relGroup.title
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Repeater {
|
||||
model: relGroup.rows.length > root.sidebarPreview && !relGroup.expanded
|
||||
? relGroup.rows.slice(0, root.sidebarPreview)
|
||||
: relGroup.rows
|
||||
delegate: SidebarLink {
|
||||
required property var modelData
|
||||
label: modelData.name
|
||||
tooltip: modelData.tooltip
|
||||
iconName: relGroup.iconName
|
||||
// POC-NOTE: the Slint row resolves the musician through
|
||||
// MusicBrainz and lands on the Qobuz artist (or its
|
||||
// MusicianPageView). Neither exists here, so the row is
|
||||
// informational — see GLUE in the handoff report.
|
||||
navigable: false
|
||||
}
|
||||
}
|
||||
Text {
|
||||
visible: relGroup.rows.length > root.sidebarPreview
|
||||
leftPadding: 8
|
||||
// Same msgid pair the page's other expanders use — no new
|
||||
// catalog entries (all 8 locales already carry these).
|
||||
text: relGroup.expanded
|
||||
? QbzSession.tr("View less", QbzSession.trRev)
|
||||
: QbzSession.tr("Load more", QbzSession.trRev)
|
||||
color: relMoreArea.containsMouse ? theme.textPrimary : theme.textSecondary
|
||||
font.pixelSize: 12
|
||||
MouseArea {
|
||||
id: relMoreArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: relGroup.toggled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +796,15 @@ Rectangle {
|
||||
onClicked: {
|
||||
var shell = root.parent
|
||||
while (shell && shell.openTextModal === undefined) shell = shell.parent
|
||||
if (shell) shell.openTextModal(artist.name || "", artist.bio || "")
|
||||
if (!shell) return
|
||||
// The Slint modal renders the attribution
|
||||
// ("Source: TiVo") as a small line under the
|
||||
// body; the shared text modal has one body
|
||||
// slot, so it rides at the end.
|
||||
var body = artist.bio || ""
|
||||
if ((artist.bioSource || "") !== "")
|
||||
body += "\n\n" + QbzSession.tr("Source", QbzSession.trRev) + ": " + artist.bioSource
|
||||
shell.openTextModal(artist.name || "", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,11 +815,12 @@ Rectangle {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
QbzCircleAction {
|
||||
name: artist.isFollowing ? "heart-filled" : "heart"
|
||||
active: artist.isFollowing === true
|
||||
readonly property bool following: root.toggleState("artist", artist.isFollowing)
|
||||
name: following ? "heart-filled" : "heart"
|
||||
active: following
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
artist.isFollowing = !artist.isFollowing
|
||||
root.setToggleState("artist", !following)
|
||||
QbzLibrary.libraryToggleFavorite("artist", artist.id)
|
||||
}
|
||||
}
|
||||
@@ -1233,6 +1450,47 @@ Rectangle {
|
||||
bottomPadding: 12
|
||||
spacing: 0
|
||||
|
||||
// ORIGIN (MusicBrainz). Gated exactly like the Slint
|
||||
// block: MB available AND (still loading OR the artist
|
||||
// actually carries a life span / location). With MB off
|
||||
// the whole block is absent — nothing was requested.
|
||||
Column {
|
||||
visible: network.mbAvailable === true
|
||||
&& (network.originLoading === true || mbOrigin.hasData === true)
|
||||
width: parent.width
|
||||
leftPadding: 14
|
||||
rightPadding: 14
|
||||
topPadding: 12
|
||||
bottomPadding: 12
|
||||
spacing: 6
|
||||
SidebarNote {
|
||||
visible: network.originLoading === true
|
||||
text: QbzSession.tr("Loading origin…", QbzSession.trRev)
|
||||
}
|
||||
OriginRow {
|
||||
visible: network.originLoading !== true && (mbOrigin.beginDate || "") !== ""
|
||||
key: mbOrigin.isPerson ? QbzSession.tr("BORN", QbzSession.trRev)
|
||||
: QbzSession.tr("FOUNDED", QbzSession.trRev)
|
||||
value: mbOrigin.beginDate || ""
|
||||
}
|
||||
OriginRow {
|
||||
visible: network.originLoading !== true && (mbOrigin.locationDisplay || "") !== ""
|
||||
key: mbOrigin.isPerson ? QbzSession.tr("BORN IN", QbzSession.trRev)
|
||||
: QbzSession.tr("FOUNDED IN", QbzSession.trRev)
|
||||
// POC-NOTE: the Slint row is clickable and opens
|
||||
// ArtistsByLocationView; that view has no port
|
||||
// here, so the affordance is left out rather than
|
||||
// rendered dead.
|
||||
value: mbOrigin.locationDisplay || ""
|
||||
}
|
||||
OriginRow {
|
||||
visible: network.originLoading !== true && (mbOrigin.endDate || "") !== ""
|
||||
key: mbOrigin.isPerson ? QbzSession.tr("DIED", QbzSession.trRev)
|
||||
: QbzSession.tr("DISBANDED", QbzSession.trRev)
|
||||
value: mbOrigin.endDate || ""
|
||||
}
|
||||
}
|
||||
|
||||
// LABELS.
|
||||
Column {
|
||||
width: parent.width
|
||||
@@ -1241,23 +1499,16 @@ Rectangle {
|
||||
topPadding: 12
|
||||
bottomPadding: 6
|
||||
spacing: 4
|
||||
Text {
|
||||
text: QbzSession.tr("LABELS", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
Text {
|
||||
SidebarSectionHeading { text: QbzSession.tr("LABELS", QbzSession.trRev) }
|
||||
SidebarNote {
|
||||
visible: labels.length === 0
|
||||
text: QbzSession.tr("No label info", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Repeater {
|
||||
model: labels
|
||||
delegate: SidebarLink {
|
||||
label: modelData.name
|
||||
tooltip: modelData.name
|
||||
iconName: "disc"
|
||||
// POC-NOTE: no label view yet.
|
||||
}
|
||||
@@ -1272,45 +1523,222 @@ Rectangle {
|
||||
topPadding: 12
|
||||
bottomPadding: 6
|
||||
spacing: 4
|
||||
Text {
|
||||
text: QbzSession.tr("SIMILAR ARTISTS", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
Text {
|
||||
SidebarSectionHeading { text: QbzSession.tr("SIMILAR ARTISTS", QbzSession.trRev) }
|
||||
SidebarNote {
|
||||
visible: similarArtists.length === 0 && QbzArtist.artistLoading
|
||||
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Repeater {
|
||||
model: similarArtists
|
||||
delegate: SidebarLink {
|
||||
label: modelData.name
|
||||
tooltip: modelData.name
|
||||
iconName: "user"
|
||||
onClicked: QbzArtist.openArtist(modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RELATIONSHIPS (MusicBrainz) — band members, the groups
|
||||
// this artist belongs to, and studio collaborators.
|
||||
Column {
|
||||
visible: network.mbAvailable === true
|
||||
&& (network.relationshipsLoading === true
|
||||
|| mbRelationships.hasData === true)
|
||||
width: parent.width
|
||||
leftPadding: 14
|
||||
rightPadding: 14
|
||||
topPadding: 12
|
||||
bottomPadding: 6
|
||||
spacing: 6
|
||||
SidebarSectionHeading { text: QbzSession.tr("RELATIONSHIPS", QbzSession.trRev) }
|
||||
SidebarNote {
|
||||
visible: network.relationshipsLoading === true
|
||||
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
||||
}
|
||||
RelationshipGroup {
|
||||
visible: network.relationshipsLoading !== true
|
||||
&& (mbRelationships.members || []).length > 0
|
||||
title: QbzSession.tr("Members & Former", QbzSession.trRev)
|
||||
rows: mbRelationships.members || []
|
||||
iconName: "user"
|
||||
expanded: root.membersExpanded
|
||||
onToggled: root.membersExpanded = !root.membersExpanded
|
||||
}
|
||||
RelationshipGroup {
|
||||
visible: network.relationshipsLoading !== true
|
||||
&& (mbRelationships.groups || []).length > 0
|
||||
title: QbzSession.tr("Member Of", QbzSession.trRev)
|
||||
rows: mbRelationships.groups || []
|
||||
iconName: "music"
|
||||
expanded: root.groupsExpanded
|
||||
onToggled: root.groupsExpanded = !root.groupsExpanded
|
||||
}
|
||||
RelationshipGroup {
|
||||
visible: network.relationshipsLoading !== true
|
||||
&& (mbRelationships.collaborators || []).length > 0
|
||||
title: QbzSession.tr("Collaborators", QbzSession.trRev)
|
||||
rows: mbRelationships.collaborators || []
|
||||
iconName: "user"
|
||||
expanded: root.collabsExpanded
|
||||
onToggled: root.collabsExpanded = !root.collabsExpanded
|
||||
}
|
||||
}
|
||||
|
||||
// YOU MAY ALSO LIKE (MusicBrainz tag discovery, validated
|
||||
// against Qobuz by the core). Rows without a resolved
|
||||
// Qobuz id stay informational instead of dead-clicking.
|
||||
Column {
|
||||
visible: network.mbAvailable === true
|
||||
&& (network.discoveryLoading === true || root.discoveryRows.length > 0)
|
||||
width: parent.width
|
||||
leftPadding: 14
|
||||
rightPadding: 14
|
||||
topPadding: 12
|
||||
bottomPadding: 12
|
||||
spacing: 4
|
||||
SidebarSectionHeading { text: QbzSession.tr("YOU MAY ALSO LIKE", QbzSession.trRev) }
|
||||
SidebarNote {
|
||||
visible: network.discoveryLoading === true && root.discoveryRows.length === 0
|
||||
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
||||
}
|
||||
Repeater {
|
||||
model: root.discoveryRows
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
// -28 = the section Column's left+right
|
||||
// padding (see OriginRow).
|
||||
width: parent ? parent.width - 28 : 0
|
||||
height: 28
|
||||
SidebarLink {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Explicit width REPLACES the component's
|
||||
// own `parent.width` binding (leaving it
|
||||
// and anchoring both edges fights it).
|
||||
width: parent.width - 26
|
||||
label: modelData.name
|
||||
tooltip: modelData.name
|
||||
iconName: "user"
|
||||
navigable: modelData.qobuzId !== ""
|
||||
onClicked: QbzArtist.openArtist(modelData.qobuzId)
|
||||
}
|
||||
Rectangle {
|
||||
id: dismissBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 4
|
||||
color: dismissArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "thumbs-down"
|
||||
width: 12
|
||||
height: 12
|
||||
tintName: dismissArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: dismissArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// Session-only: drop the row now. The
|
||||
// Slint app also persists it under the
|
||||
// discovery tag; that store is not open
|
||||
// in this port (see the handoff report).
|
||||
onClicked: {
|
||||
var d = root.dismissedDiscovery
|
||||
d[modelData.mbid] = true
|
||||
root.dismissedDiscovery = Object.assign({}, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Magazine tab body — POC-NOTE placeholder (the Stories CMS is
|
||||
// a separate subsystem).
|
||||
Item {
|
||||
// Magazine tab body — Qobuz editorial story teasers (limit 2,
|
||||
// like the official client). A story opens in the system browser.
|
||||
Flickable {
|
||||
visible: root.netTab === "magazine"
|
||||
width: parent.width
|
||||
height: parent.height - 45
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 28
|
||||
text: "QBZ Qt POC — the Magazine feed is not wired yet"
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: magBody.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: magBody
|
||||
width: parent.width
|
||||
padding: 12
|
||||
spacing: 10
|
||||
|
||||
SidebarNote {
|
||||
visible: artist.storiesLoading === true && stories.length === 0
|
||||
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
||||
}
|
||||
SidebarNote {
|
||||
visible: artist.storiesLoading !== true && stories.length === 0
|
||||
text: QbzSession.tr("No stories for this artist", QbzSession.trRev)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: stories
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
width: magBody.width - 24
|
||||
height: storyCol.implicitHeight
|
||||
radius: 8
|
||||
color: storyArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
Column {
|
||||
id: storyCol
|
||||
width: parent.width
|
||||
padding: 6
|
||||
spacing: 6
|
||||
// 1:1 square thumbnail, height tracks width.
|
||||
Rectangle {
|
||||
visible: (modelData.artUrl || "") !== ""
|
||||
width: storyCol.width - 12
|
||||
height: visible ? width : 0
|
||||
radius: 6
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[modelData.artUrl] || ""
|
||||
radius: 6
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: storyCol.width - 12
|
||||
text: modelData.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 13
|
||||
font.weight: theme.weightSemibold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Text {
|
||||
visible: (modelData.author || "") !== ""
|
||||
width: storyCol.width - 12
|
||||
text: modelData.author
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: storyArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if ((modelData.url || "") !== "") Qt.openUrlExternally(modelData.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1361,7 +1789,7 @@ Rectangle {
|
||||
{ "label": QbzSession.tr("Create Artist Collection", QbzSession.trRev), "icon": "library-big", "action": "stub" },
|
||||
{ "label": QbzSession.tr("Artist Scene", QbzSession.trRev), "icon": "map-pin", "action": "stub" },
|
||||
{ "label": QbzSession.tr("Share", QbzSession.trRev), "icon": "link", "action": "stub" },
|
||||
{ "label": artist.isPinned ? QbzSession.tr("Unpin", QbzSession.trRev) : QbzSession.tr("Pin", QbzSession.trRev), "icon": artist.isPinned ? "pin-filled" : "pin", "action": "pin" },
|
||||
{ "label": root.toggleState("artistPin", artist.isPinned) ? QbzSession.tr("Unpin", QbzSession.trRev) : QbzSession.tr("Pin", QbzSession.trRev), "icon": root.toggleState("artistPin", artist.isPinned) ? "pin-filled" : "pin", "action": "pin" },
|
||||
]
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
@@ -1392,7 +1820,7 @@ Rectangle {
|
||||
onClicked: {
|
||||
overflowMenu.close()
|
||||
if (modelData.action === "pin") {
|
||||
artist.isPinned = !artist.isPinned
|
||||
root.setToggleState("artistPin", !root.toggleState("artistPin", artist.isPinned))
|
||||
QbzLibrary.togglePin("artist", artist.id, artist.name, "", artist.artUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user