ONE component instantiated twice, because it is one form twice. Every difference between the protocols is a property set at the call site, so they are visible in one place instead of drifting across two files: "Test" proves the address for Jellyfin and only "a Subsonic server answered" for Subsonic (which has no credential-free endpoint), the password is discarded for Jellyfin and kept for Subsonic (no session, md5(password+salt) per request), and the sync cost is stated up front — 45.8 s per 4924 tracks against 0.81 s per 6678 — so a long sweep does not read as a hang. The credential never leaves Rust; the panel gets `hasCredential`. Assets: the colour marks for cards, the monochrome ones for rows. Both mono files arrived as BLACK ink and are invisible on a dark theme untinted, so they were normalised to the #ffffff master ink and joined `icon_tint_qt::MASTERS` — the port's only shader-free way to recolour an SVG per theme. The Jellyfin colour asset had a white rounded-rect plate behind the mark; it was stripped, because every other brand mark here is transparent and a white tile beside them reads as a rendering bug. Rendered with Qt itself and looked at before any of it was wired. CAVEAT, recorded in SourceIcon: the Subsonic mark is NAVIDROME's logo, and `subsonic` covers Gonic, Airsonic, Astiga and Ampache too. `ping` reports the server's own type and it is already persisted, so branching later is cheap — a known wrongness, not an unnoticed one. THREE BUGS THAT ONLY THE RUNNING WINDOW COULD SHOW, all found by driving it under Xvfb rather than by reading the log: 1. `MediaServerSettings is not a type` — a .qml missing from build.rs is not in the QML module. It resolves fine on disk, compiles nothing, and blanks the WHOLE settings page at runtime. All three existing audits passed. `qml_module_registration_audit.py` is the missing check and is now in the gate. 2. The toggle would not move. The panel reads its state from the settings DOCUMENT, unlike Plex's toggle which reads a bridge property, so every mutation has to republish it — connect, enable, disconnect and the end of a sweep now do. 3. Every Jellyfin album in the grid drew the LOCAL HARD-DRIVE badge: `local_rows::badge_source` had never been taught the two words and folded them into `_ => local`. Not cosmetic — that string is what the source chips filter on. `play_single_from_source` also grew the arm it needed: a media-server row from the recently-played rail has no library.db row to mint a LocalTrack from, so it resolves through `registry.tracks_of` instead. END TO END, from the UI, against the real server: toggle on -> type the address -> Connect -> "jellyfin sync: 500/4924 ... 4924 saved, 0 pruned, 4924 cached" -> Local Library shows 461 albums with their covers and the Jellyfin badge. The cached quality distribution is identical to what the server reported in the original protocol sweep. 428/428. cc
219 lines
8.2 KiB
QML
219 lines
8.2 KiB
QML
// Compact album row for the LIST arm of the Albums / Folders-flat / Artists
|
|
// surfaces (AlbumCollectionView's list mode, mounted by
|
|
// LocalLibraryView.slint:1255 with show-source / show-source-badge on).
|
|
//
|
|
// 56px: cover, title + artist, year, track count, quality mark, the source
|
|
// column and the trailing ⋯ overflow. Multi-select puts the shared checkbox
|
|
// in front of the cover, as the collection view does.
|
|
//
|
|
// The ⋯ menu is AlbumListRow.slint:381 minus its two Qobuz-only entries: the
|
|
// favourite heart (local albums are not catalog favourites) and "Block this
|
|
// album" (which the Slint itself hides for source local/plex, :435). What is
|
|
// left is the five entries Slint always shows — Open album, Play, Play next,
|
|
// Play later, Add to queue — and all five are wired. Right-clicking the row
|
|
// opens the same menu at the pointer.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../../controls"
|
|
import "../../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property var item: ({})
|
|
property string artSource: ""
|
|
/// The LocalLibraryView root — the shared skeleton pulse, the per-item
|
|
/// artwork gate and the settle bound all live there (never duplicated
|
|
/// per row, so the rule cannot drift between surfaces).
|
|
property var view: null
|
|
property bool showSource: true
|
|
property bool selectMode: false
|
|
property bool checked: false
|
|
signal opened()
|
|
signal playRequested()
|
|
signal enqueueRequested(string mode)
|
|
signal toggleSelect()
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
height: 56
|
|
radius: 6
|
|
color: rowArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
|
|
MouseArea {
|
|
id: rowArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) {
|
|
if (mouse.button === Qt.RightButton) {
|
|
rowMenu.openAtCursor(rowArea, mouse.x, mouse.y)
|
|
return
|
|
}
|
|
if (root.selectMode) root.toggleSelect()
|
|
else root.opened()
|
|
}
|
|
onDoubleClicked: if (!root.selectMode) root.playRequested()
|
|
}
|
|
|
|
CardMenu {
|
|
id: rowMenu
|
|
menuWidth: 196
|
|
entries: [
|
|
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
|
|
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
|
{ "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) {
|
|
if (a === "open") root.opened()
|
|
else if (a === "play") root.playRequested()
|
|
else root.enqueueRequested(a)
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 12
|
|
spacing: 12
|
|
|
|
Item {
|
|
visible: root.selectMode
|
|
width: visible ? 16 : 0
|
|
height: parent.height
|
|
SelectCheck {
|
|
anchors.centerIn: parent
|
|
on: root.checked
|
|
onToggled: root.toggleSelect()
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 40
|
|
height: 40
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: 4
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
RoundedImage {
|
|
id: rowArt
|
|
anchors.fill: parent
|
|
source: root.artSource
|
|
radius: 4
|
|
}
|
|
// Per-item: hands over the instant the art is actually ON the
|
|
// canvas (`rowArt.ready`, not "the path landed"); settles out
|
|
// when the album simply has none (local artwork drops such keys).
|
|
QbzSkeleton {
|
|
variant: "art"
|
|
anchors.fill: parent
|
|
blockRadius: 4
|
|
pending: root.view ? root.view.artWanted(root.item.artKey) : false
|
|
coverReady: rowArt.ready
|
|
phase: root.view ? root.view.skelPhase : false
|
|
settleMs: root.view ? root.view.artSettleMs : 0
|
|
settleHold: root.view ? root.view.artPulse : false
|
|
}
|
|
}
|
|
Column {
|
|
width: parent.width - 40 - 70 - 90 - 92 - 32
|
|
- (root.showSource ? 34 : 0)
|
|
- (root.selectMode ? 28 : 0) - 6 * 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width
|
|
text: root.item.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: 14
|
|
font.weight: theme.weightMedium
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: root.item.artist || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: 12
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
Text {
|
|
width: 70
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.item.year || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: 12
|
|
}
|
|
Text {
|
|
width: 90
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: (root.item.trackCount || 0) + " "
|
|
+ QbzSession.tr("tracks", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 12
|
|
}
|
|
Item {
|
|
width: 92
|
|
height: parent.height
|
|
QualityMini {
|
|
tier: root.item.qualityTier || ""
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
Item {
|
|
visible: root.showSource
|
|
width: visible ? 34 : 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
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
// Trailing ⋯ overflow (AlbumListRow.slint:360).
|
|
Rectangle {
|
|
width: 32
|
|
height: 32
|
|
radius: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: moreArea.containsMouse ? theme.surfaceElevated : "transparent"
|
|
QbzIcon {
|
|
name: "ellipsis"
|
|
width: 18
|
|
height: 18
|
|
anchors.centerIn: parent
|
|
tintName: moreArea.containsMouse ? "textPrimary" : "muted"
|
|
}
|
|
MouseArea {
|
|
id: moreArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
|
}
|
|
}
|
|
}
|
|
}
|