fix(library): unify album versions across sources
Associate physical copies only when normalized album identity and strong track-title/duration overlap agree. Keep every source visible on one card and resolve each authoritative album id into the existing version cache. Order versions by delivered quality and then track count, expose inferred edition labels and full format metadata, and keep source filters aware of every represented copy. cx
This commit is contained in:
13 files changed
+735
-90
No files matched your search
@@ -72,6 +72,11 @@ Rectangle {
|
||||
// (AlbumCard.slint:434 keeps the entry off local/plex rows). Hosts that
|
||||
// want the badge hidden set `showSourceBadge`, never `source: ""`.
|
||||
property string source: ""
|
||||
// Local Library logical albums can have one physical copy on several
|
||||
// sources. Other hosts leave this empty and retain the scalar badge.
|
||||
property var sources: []
|
||||
readonly property var badgeSources: root.sources && root.sources.length > 0
|
||||
? root.sources : (root.source !== "" ? [root.source] : [])
|
||||
/// Source words that mean "this album does not live in the Qobuz catalog".
|
||||
/// Same fold as `SourceIcon.isSubsonic`: a row stamped with a Subsonic
|
||||
/// BRAND is still a server row, so it must not be offered a catalog-only
|
||||
@@ -620,40 +625,39 @@ Rectangle {
|
||||
// `hard-drive` for Plex — a blue hard drive where the design calls
|
||||
// for the Plex mark.
|
||||
Rectangle {
|
||||
visible: root.showSourceBadge && root.source !== ""
|
||||
visible: root.showSourceBadge && root.badgeSources.length > 0
|
||||
x: parent.width - width - 6
|
||||
y: parent.height - height - 6
|
||||
width: 24
|
||||
width: sourceBadgeRow.implicitWidth
|
||||
height: 24
|
||||
radius: 4
|
||||
// .slint:335-338. Slint literals are #RRGGBBAA, Qt's are
|
||||
// #AARRGGBB: #1e1400d9 -> #d91e1400, #000000b3 -> #b3000000,
|
||||
// #eab30880 -> #80eab308.
|
||||
// qobuz_purchase -> §8.7 gold chip (dark-amber translucent
|
||||
// fill + 1px gold border)
|
||||
// qobuz_download -> no chip at all (the wordmark stands
|
||||
// alone at 22px)
|
||||
// everything else -> the near-black chip.
|
||||
color: root.source === "qobuz_purchase" ? "#d91e1400"
|
||||
: (root.source === "offline" ? "transparent" : "#b3000000")
|
||||
border.width: root.source === "qobuz_purchase" ? 1 : 0
|
||||
border.color: "#80eab308"
|
||||
SourceIcon {
|
||||
kind: root.source
|
||||
// .slint:347-349 — one size per arm: plex 16, download 22,
|
||||
// qobuz/purchase 18, local 14. "offline" IS the Qt word for
|
||||
// `qobuz_download` (local_rows.rs `badge_source`).
|
||||
glyphSize: 14
|
||||
plexSize: 16
|
||||
qobuzSize: root.source === "offline" ? 22 : 18
|
||||
// .slint:345-346 — the monochrome glyph is #ffffff here (the
|
||||
// chip is dark under every theme), not the rows' muted.
|
||||
localTint: "white"
|
||||
// The .slint centres with explicit pixel rounding
|
||||
// (:351-352); anchors.centerIn would leave a half pixel on
|
||||
// the odd sizes and blur the mark.
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
color: "transparent"
|
||||
Row {
|
||||
id: sourceBadgeRow
|
||||
height: 24
|
||||
spacing: 3
|
||||
Repeater {
|
||||
model: root.badgeSources
|
||||
delegate: Rectangle {
|
||||
id: sourceBadge
|
||||
required property string modelData
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 4
|
||||
color: modelData === "qobuz_purchase" ? "#d91e1400"
|
||||
: (modelData === "offline" ? "transparent" : "#b3000000")
|
||||
border.width: modelData === "qobuz_purchase" ? 1 : 0
|
||||
border.color: "#80eab308"
|
||||
SourceIcon {
|
||||
kind: sourceBadge.modelData
|
||||
glyphSize: 14
|
||||
plexSize: 16
|
||||
qobuzSize: sourceBadge.modelData === "offline" ? 22 : 18
|
||||
localTint: "white"
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -501,6 +501,16 @@ Rectangle {
|
||||
|
||||
// Quality / format / source chips. Each SECTION is an OR within itself
|
||||
// and an AND across sections — an empty section means "any".
|
||||
function sourceBucket(word) {
|
||||
var source = (word || "local").toLowerCase()
|
||||
if (source === "qobuz_purchase" || source === "qobuz_download")
|
||||
return "offline"
|
||||
if (source === "navidrome" || source === "gonic"
|
||||
|| source === "airsonic" || source === "astiga")
|
||||
return "subsonic"
|
||||
return source
|
||||
}
|
||||
|
||||
function applyFilter(rows) {
|
||||
if (filterCount === 0) return rows
|
||||
var qAny = filter.hires || filter.cd || filter.lossy
|
||||
@@ -536,16 +546,16 @@ Rectangle {
|
||||
// closed: Tauri's three arms never mention `qobuz_purchase`,
|
||||
// so a raw word reaching here matches no chip and ticking ANY
|
||||
// source filter hides every purchased album, silently.
|
||||
var src = (r.source || "local").toLowerCase()
|
||||
if (src === "qobuz_purchase" || src === "qobuz_download") src = "offline"
|
||||
// Every brand spelling of a Subsonic server is ONE chip, the
|
||||
// same fold `SourceId::from_word` applies. A row stamped
|
||||
// "navidrome" that matched no chip would VANISH the moment any
|
||||
// source filter was ticked — the §10-H shape above, in a new
|
||||
// source.
|
||||
if (src === "navidrome" || src === "gonic" || src === "airsonic"
|
||||
|| src === "astiga") src = "subsonic"
|
||||
if (!filter[src]) continue
|
||||
var sources = r.sources && r.sources.length > 0
|
||||
? r.sources : [r.source || "local"]
|
||||
var sourceMatches = false
|
||||
for (var si = 0; si < sources.length; si++) {
|
||||
if (filter[sourceBucket(sources[si])] === true) {
|
||||
sourceMatches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!sourceMatches) continue
|
||||
}
|
||||
out.push(r)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ Item {
|
||||
/// churn the constant cell count removed.
|
||||
readonly property var emptySlot: ({
|
||||
"id": "", "title": "", "artist": "", "year": "",
|
||||
"qualityTier": "", "artKey": "", "source": "", "sourceRaw": ""
|
||||
"qualityTier": "", "artKey": "", "source": "", "sourceRaw": "",
|
||||
"sources": []
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -496,6 +497,7 @@ Item {
|
||||
// about the same album.
|
||||
source: cardCell.slot.sourceRaw
|
||||
|| cardCell.slot.source
|
||||
sources: cardCell.slot.sources || []
|
||||
showSourceBadge: root.showSource
|
||||
// SELECT MODE IS THE CARD'S (AlbumCard's
|
||||
// `selectMode`/`selected`/`selectToggled`, the
|
||||
|
||||
@@ -167,31 +167,34 @@ Rectangle {
|
||||
}
|
||||
Item {
|
||||
visible: root.showSource
|
||||
width: visible ? 34 : 0
|
||||
readonly property var sourceValues: root.item.sources
|
||||
&& root.item.sources.length > 0
|
||||
? root.item.sources
|
||||
: ((root.item.sourceRaw || root.item.source || "") !== ""
|
||||
? [root.item.sourceRaw || root.item.source] : [])
|
||||
width: visible ? Math.max(34, sourceIcons.implicitWidth) : 0
|
||||
height: parent.height
|
||||
// AlbumListRow.slint:308-322 — through controls/SourceIcon.qml,
|
||||
// never QbzIcon: the Plex and Qobuz marks are MULTI-COLOUR and a
|
||||
// tint flattens them to a silhouette. This row used to draw an
|
||||
// accent-tinted `hard-drive` for Plex (a blue hard drive) and
|
||||
// `cloud-download` for an offline copy.
|
||||
SourceIcon {
|
||||
visible: (root.item.source || "") !== ""
|
||||
// `sourceRaw` FIRST (contract §D.1): it carries
|
||||
// `qobuz_purchase`, which `source` folds into "offline" for the
|
||||
// benefit of the source chips. Without this the LIST mode of
|
||||
// the very same album loses the gold mark the GRID shows.
|
||||
kind: root.item.sourceRaw || root.item.source || ""
|
||||
// A dense ROW: the media marks draw monochrome and tinted, like the
|
||||
// hard-drive beside them. Colour logos are for cards — a list of
|
||||
// them fights the text it labels.
|
||||
mono: true
|
||||
// .slint:319 — local 15, the marks 16; muted local tint.
|
||||
glyphSize: 15
|
||||
plexSize: 16
|
||||
qobuzSize: 16
|
||||
localTint: "muted"
|
||||
x: 0 // .slint:321
|
||||
Row {
|
||||
id: sourceIcons
|
||||
spacing: 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Repeater {
|
||||
model: sourceIcons.parent.sourceValues
|
||||
delegate: SourceIcon {
|
||||
required property string modelData
|
||||
kind: modelData
|
||||
mono: true
|
||||
glyphSize: 15
|
||||
plexSize: 16
|
||||
qobuzSize: 16
|
||||
localTint: "muted"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trailing ⋯ overflow (AlbumListRow.slint:360).
|
||||
|
||||
@@ -15,7 +15,7 @@ import "../../theme"
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
/// [{ label, source }]
|
||||
/// [{ version, trackCount, quality, source }]
|
||||
property var versions: []
|
||||
property int current: 0
|
||||
signal picked(int index)
|
||||
@@ -24,6 +24,17 @@ Rectangle {
|
||||
|
||||
readonly property var currentVersion: versions[current] || ({})
|
||||
|
||||
function optionText(version) {
|
||||
var parts = []
|
||||
if ((version.version || "") !== "")
|
||||
parts.push(version.version)
|
||||
parts.push((version.trackCount || 0) + " "
|
||||
+ QbzSession.tr("tracks", QbzSession.trRev))
|
||||
if ((version.quality || "") !== "")
|
||||
parts.push(version.quality)
|
||||
return parts.join(" · ")
|
||||
}
|
||||
|
||||
height: 30
|
||||
width: row.width
|
||||
radius: 6
|
||||
@@ -48,7 +59,7 @@ Rectangle {
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.currentVersion.label || ""
|
||||
text: root.optionText(root.currentVersion)
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
@@ -70,7 +81,7 @@ Rectangle {
|
||||
|
||||
QbzContextMenu {
|
||||
id: menu
|
||||
menuWidth: 280
|
||||
menuWidth: 380
|
||||
Repeater {
|
||||
model: root.versions
|
||||
delegate: Rectangle {
|
||||
@@ -95,7 +106,7 @@ Rectangle {
|
||||
Text {
|
||||
width: parent.width - 22 - (opt.index === root.current ? 20 : 0)
|
||||
height: parent.height
|
||||
text: opt.modelData.label || ""
|
||||
text: root.optionText(opt.modelData)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontLegal
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Reference in new issue
Block a user