feat(qt): the Releases list shows each release's label, and album menus open it
Library feed album items carry the catalog label id next to its name. Library > Releases (list) gains a LABEL column between the item and its quality — a link to the label page when the id is known, muted text otherwise — hidden on the label page itself and on narrow kiosk rows. AlbumListRow and AlbumCard menus gain "View label" (the card only where its host passes the id: the library grid does). The library's release-wide availability helper is shared with the playlist rows for the next change. cc
This commit is contained in:
6 files changed
+64
-4
No files matched your search
@@ -40,6 +40,9 @@ Rectangle {
|
|||||||
property string title: ""
|
property string title: ""
|
||||||
property string artist: ""
|
property string artist: ""
|
||||||
property string artistId: ""
|
property string artistId: ""
|
||||||
|
/// The release's label id when the host knows it (library feed rows do):
|
||||||
|
/// the "View label" menu entry (2026-09-13). "" = no entry.
|
||||||
|
property string labelId: ""
|
||||||
// Hosts with only a display snapshot (Home's persisted Pinned row) can
|
// Hosts with only a display snapshot (Home's persisted Pinned row) can
|
||||||
// still make the artist line a link and resolve the destination lazily.
|
// still make the artist line a link and resolve the destination lazily.
|
||||||
// Normal catalog cards keep the direct artistId path and pay no lookup.
|
// Normal catalog cards keep the direct artistId path and pay no lookup.
|
||||||
@@ -391,6 +394,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
// Host tail (see `extraMenuEntries`). `concat` so the host's array is
|
// Host tail (see `extraMenuEntries`). `concat` so the host's array is
|
||||||
// never mutated — it is usually a binding's return value.
|
// never mutated — it is usually a binding's return value.
|
||||||
|
if (root.labelId !== "")
|
||||||
|
m.push({ "label": t("View label", r), "icon": "tags", "action": "view-label" })
|
||||||
var extra = root.extraMenuEntries || []
|
var extra = root.extraMenuEntries || []
|
||||||
return extra.length > 0 ? m.concat(extra) : m
|
return extra.length > 0 ? m.concat(extra) : m
|
||||||
}
|
}
|
||||||
@@ -415,6 +420,7 @@ Rectangle {
|
|||||||
// denormalized snapshot and a file:// cache path is dead on any other
|
// denormalized snapshot and a file:// cache path is dead on any other
|
||||||
// machine, the same reason the pin payload uses it.
|
// machine, the same reason the pin payload uses it.
|
||||||
if (a === "favorite") { root.toggleFavorite(); return }
|
if (a === "favorite") { root.toggleFavorite(); return }
|
||||||
|
if (a === "view-label") { QbzHome.openLabel(root.labelId); return }
|
||||||
if (a === "find-release") {
|
if (a === "find-release") {
|
||||||
QbzTrackReplace.openRelease(JSON.stringify({
|
QbzTrackReplace.openRelease(JSON.stringify({
|
||||||
"targetKind": "album",
|
"targetKind": "album",
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ Rectangle {
|
|||||||
|
|
||||||
// AlbumListCols: art 52, quality 150, year 64, overflow 36, gap 12.
|
// AlbumListCols: art 52, quality 150, year 64, overflow 36, gap 12.
|
||||||
readonly property int colArt: 52
|
readonly property int colArt: 52
|
||||||
|
/// The release's label, between the item and its quality (2026-09-13);
|
||||||
|
/// off on the label page itself, where every row would repeat the same.
|
||||||
|
property bool showLabel: true
|
||||||
|
readonly property int colLabel: showLabel ? 150 : 0
|
||||||
readonly property int colQuality: 150
|
readonly property int colQuality: 150
|
||||||
readonly property int colYear: 64
|
readonly property int colYear: 64
|
||||||
readonly property int colOverflow: 36
|
readonly property int colOverflow: 36
|
||||||
@@ -56,10 +60,16 @@ Rectangle {
|
|||||||
ColLabel {
|
ColLabel {
|
||||||
width: parent.width - root.colArt - root.colQuality - root.colYear
|
width: parent.width - root.colArt - root.colQuality - root.colYear
|
||||||
- root.colOverflow - 4 * root.colGap
|
- root.colOverflow - 4 * root.colGap
|
||||||
|
- (root.showLabel ? root.colLabel + root.colGap : 0)
|
||||||
- (root.selectMode ? 18 + root.colGap : 0)
|
- (root.selectMode ? 18 + root.colGap : 0)
|
||||||
text: QbzSession.tr("ITEM", QbzSession.trRev)
|
text: QbzSession.tr("ITEM", QbzSession.trRev)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
ColLabel {
|
||||||
|
visible: root.showLabel
|
||||||
|
width: root.colLabel
|
||||||
|
text: QbzSession.tr("LABEL", QbzSession.trRev)
|
||||||
|
}
|
||||||
ColLabel {
|
ColLabel {
|
||||||
width: root.colQuality
|
width: root.colQuality
|
||||||
text: QbzSession.tr("QUALITY", QbzSession.trRev)
|
text: QbzSession.tr("QUALITY", QbzSession.trRev)
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ Rectangle {
|
|||||||
|
|
||||||
// Same columns (and widths) as AlbumListHeader.qml.
|
// Same columns (and widths) as AlbumListHeader.qml.
|
||||||
readonly property int colArt: 52
|
readonly property int colArt: 52
|
||||||
|
/// See AlbumListHeader.showLabel.
|
||||||
|
property bool showLabel: true
|
||||||
|
readonly property int colLabel: (!root.showLabel || (root.kioskHost && width < 500)) ? 0 : 150
|
||||||
readonly property int colQuality: root.kioskHost && width < 500 ? 0 : 150
|
readonly property int colQuality: root.kioskHost && width < 500 ? 0 : 150
|
||||||
readonly property int colYear: root.kioskHost && width < 500 ? 0 : 64
|
readonly property int colYear: root.kioskHost && width < 500 ? 0 : 64
|
||||||
readonly property int colOverflow: root.kioskHost ? 44 : 36
|
readonly property int colOverflow: root.kioskHost ? 44 : 36
|
||||||
@@ -107,7 +110,7 @@ Rectangle {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (root.selectMode) root.toggleSelect(mouse.modifiers)
|
if (root.selectMode) root.toggleSelect(mouse.modifiers)
|
||||||
else if (!root.pulledDead) QbzAlbum.openAlbum(root.item.id || "")
|
else if (!root.pulledDead) QbzAlbum.openAlbumFrom(root.item.id || "", root.item.title || "", root.item.artist || "")
|
||||||
}
|
}
|
||||||
// Touch has no right button. Desktop-inert: `pressAndHold` never
|
// Touch has no right button. Desktop-inert: `pressAndHold` never
|
||||||
// fires for a mouse press that is released normally, and the guard
|
// fires for a mouse press that is released normally, and the guard
|
||||||
@@ -160,13 +163,17 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
if (!root.pulledDead && root.catalogRow)
|
if (!root.pulledDead && root.catalogRow)
|
||||||
m.push({ "label": t("Block this album", r), "icon": "blind-eye", "action": "block" })
|
m.push({ "label": t("Block this album", r), "icon": "blind-eye", "action": "block" })
|
||||||
|
// The label page outlives a pulled release, so this one stays.
|
||||||
|
if ((root.item.labelId || "") !== "")
|
||||||
|
m.push({ "label": t("View label", r), "icon": "tags", "action": "view-label" })
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
function menuAction(a) {
|
function menuAction(a) {
|
||||||
var id = root.item.id || ""
|
var id = root.item.id || ""
|
||||||
if (id === "") return
|
if (id === "") return
|
||||||
|
if (a === "view-label") { QbzHome.openLabel(root.item.labelId || ""); return }
|
||||||
if (root.pulledDead) return
|
if (root.pulledDead) return
|
||||||
if (a === "open") QbzAlbum.openAlbum(id)
|
if (a === "open") QbzAlbum.openAlbumFrom(id, root.item.title || "", root.item.artist || "")
|
||||||
else if (a === "play") QbzPlayer.playAlbum(id)
|
else if (a === "play") QbzPlayer.playAlbum(id)
|
||||||
// `artUrl`, never `artPath`: the store keeps a denormalized cover url
|
// `artUrl`, never `artPath`: the store keeps a denormalized cover url
|
||||||
// and a file:// cache path is dead on any other machine.
|
// and a file:// cache path is dead on any other machine.
|
||||||
@@ -274,6 +281,7 @@ Rectangle {
|
|||||||
// multi-select is switched on.
|
// multi-select is switched on.
|
||||||
width: parent.width - root.colArt - root.colQuality - root.colYear
|
width: parent.width - root.colArt - root.colQuality - root.colYear
|
||||||
- root.colOverflow - 4 * root.colGap
|
- root.colOverflow - 4 * root.colGap
|
||||||
|
- (root.colLabel > 0 ? root.colLabel + root.colGap : 0)
|
||||||
- (root.selectMode ? 18 + root.colGap : 0)
|
- (root.selectMode ? 18 + root.colGap : 0)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 2
|
spacing: 2
|
||||||
@@ -305,6 +313,32 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LABEL — the release's label, a link to its page when the feed
|
||||||
|
// carries the id (2026-09-13). Zero-width (and skipped by the Row)
|
||||||
|
// where the column is off.
|
||||||
|
Item {
|
||||||
|
visible: root.colLabel > 0
|
||||||
|
width: root.colLabel
|
||||||
|
height: parent.height
|
||||||
|
Text {
|
||||||
|
id: labelText
|
||||||
|
width: parent.width
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.item.label || ""
|
||||||
|
color: labelArea.containsMouse && labelArea.enabled ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
MouseArea {
|
||||||
|
id: labelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: (root.item.labelId || "") !== ""
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: QbzHome.openLabel(root.item.labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// QUALITY — the SAME badge the album page shows (tier label over the
|
// QUALITY — the SAME badge the album page shows (tier label over the
|
||||||
// exact bit-depth / sample-rate line).
|
// exact bit-depth / sample-rate line).
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ Rectangle {
|
|||||||
height: root.kioskHost && (cr.kind === "album" || cr.kind === "playlist") ? 64 : 246
|
height: root.kioskHost && (cr.kind === "album" || cr.kind === "playlist") ? 64 : 246
|
||||||
|
|
||||||
KioskCoverSource { id: cellArt; remote: root.kioskHost ? (crCell.modelData.artUrl || "") : ""; local: crCell.modelData.artPath || ""; edge: 44 }
|
KioskCoverSource { id: cellArt; remote: root.kioskHost ? (crCell.modelData.artUrl || "") : ""; local: crCell.modelData.artPath || ""; edge: 44 }
|
||||||
Component { id: crKioskAlbum; AlbumListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
Component { id: crKioskAlbum; AlbumListRow { kioskHost: true; showLabel: false; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
||||||
Component { id: crKioskPlaylist; PlaylistListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
Component { id: crKioskPlaylist; PlaylistListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
||||||
Component {
|
Component {
|
||||||
id: crAlbum
|
id: crAlbum
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Item {
|
|||||||
title: cell.item.title
|
title: cell.item.title
|
||||||
artist: cell.item.artist
|
artist: cell.item.artist
|
||||||
artistId: cell.item.artistId
|
artistId: cell.item.artistId
|
||||||
|
labelId: cell.item.labelId || ""
|
||||||
genre: cell.item.genre
|
genre: cell.item.genre
|
||||||
year: cell.item.year
|
year: cell.item.year
|
||||||
qualityTier: cell.item.qualityTier
|
qualityTier: cell.item.qualityTier
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ pub struct FeedItem {
|
|||||||
pub genre: String,
|
pub genre: String,
|
||||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
/// Catalog label id behind `label` ("" when the release carries none) —
|
||||||
|
/// the Releases list column and the "View label" menu entry open it.
|
||||||
|
pub label_id: String,
|
||||||
#[serde(default, rename = "updatedAt", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "updatedAt", skip_serializing_if = "Option::is_none")]
|
||||||
pub updated_at: Option<i64>,
|
pub updated_at: Option<i64>,
|
||||||
#[serde(default, rename = "trackCount", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "trackCount", skip_serializing_if = "Option::is_none")]
|
||||||
@@ -722,6 +725,11 @@ fn map_album(album: Album, ready_offline_tracks: usize) -> FeedItem {
|
|||||||
subtitle: artist.clone(),
|
subtitle: artist.clone(),
|
||||||
artist,
|
artist,
|
||||||
artist_id: album.artist.id.to_string(),
|
artist_id: album.artist.id.to_string(),
|
||||||
|
label_id: album
|
||||||
|
.label
|
||||||
|
.as_ref()
|
||||||
|
.map(|label| label.id.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
label: album.label.map(|label| label.name).unwrap_or_default(),
|
label: album.label.map(|label| label.name).unwrap_or_default(),
|
||||||
duration_secs: album.duration.unwrap_or(0),
|
duration_secs: album.duration.unwrap_or(0),
|
||||||
track_count: Some(track_count as u32),
|
track_count: Some(track_count as u32),
|
||||||
@@ -1009,7 +1017,7 @@ async fn probe_release_availability(
|
|||||||
/// Resolve release-wide availability only for favourite tracks already known
|
/// Resolve release-wide availability only for favourite tracks already known
|
||||||
/// to be unavailable. Explicit embedded answers cost no request; unknown album
|
/// to be unavailable. Explicit embedded answers cost no request; unknown album
|
||||||
/// ids are de-duplicated and probed with a bounded fan-out.
|
/// ids are de-duplicated and probed with a bounded fan-out.
|
||||||
async fn unavailable_release_ids(
|
pub(crate) async fn unavailable_release_ids(
|
||||||
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
||||||
tracks: &[Track],
|
tracks: &[Track],
|
||||||
) -> HashSet<String> {
|
) -> HashSet<String> {
|
||||||
@@ -1662,6 +1670,7 @@ fn map_purchased_album(a: qbz_models::PurchaseAlbum) -> FeedItem {
|
|||||||
),
|
),
|
||||||
added_at: a.purchased_at.unwrap_or(0),
|
added_at: a.purchased_at.unwrap_or(0),
|
||||||
label: a.label.as_ref().map(|label| label.name.clone()).unwrap_or_default(),
|
label: a.label.as_ref().map(|label| label.name.clone()).unwrap_or_default(),
|
||||||
|
label_id: a.label.as_ref().map(|label| label.id.to_string()).unwrap_or_default(),
|
||||||
duration_secs: a.duration.unwrap_or(0),
|
duration_secs: a.duration.unwrap_or(0),
|
||||||
track_count: a.tracks_count,
|
track_count: a.tracks_count,
|
||||||
genre: a.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
|
genre: a.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
|
||||||
|
|||||||
Reference in new issue
Block a user