Files
qbz/crates/qbz-qt/qml/views/local/VersionPicker.qml
T
vicrodh 3cfce8a769 feat(qt): the media-server settings panel, driven and verified in the window
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
2026-08-20 10:57:28 -06:00

124 lines
4.2 KiB
QML

// Local album version picker (album/LocalAlbumView.slint:41 VersionPicker) —
// shown only when an album has several PHYSICAL copies (distinct source
// folders), so two copies never merge into a duplicated track list.
//
// Sized like QbzSelect `sm` (30px, radius 6, 10px left pad) so it reads as
// one of the small selects rather than towering over them: source icon +
// label + chevron, with a 280px dropdown that repeats the icon and ticks the
// current entry.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Rectangle {
id: root
/// [{ label, source }]
property var versions: []
property int current: 0
signal picked(int index)
QbzTheme { id: theme }
readonly property var currentVersion: versions[current] || ({})
height: 30
width: row.width
radius: 6
color: pickArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
Row {
id: row
height: parent.height
leftPadding: 10
rightPadding: 8
spacing: 7
SourceIcon {
anchors.verticalCenter: parent.verticalCenter
kind: root.currentVersion.source || "local"
// 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
// No size overrides on purpose: LocalAlbumView.slint:24-26 draws
// ALL THREE kinds at a flat 14px here, which is SourceIcon's
// default (the row glyphs are the ones that grow the marks).
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.currentVersion.label || ""
color: theme.textSecondary
font.pixelSize: theme.fontLegal
}
QbzIcon {
anchors.verticalCenter: parent.verticalCenter
name: "chevron-down"
width: 12
height: 12
tintName: "muted"
}
}
MouseArea {
id: pickArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: menu.openBelowRight(pickArea)
}
QbzContextMenu {
id: menu
menuWidth: 280
Repeater {
model: root.versions
delegate: Rectangle {
id: opt
required property var modelData
required property int index
width: parent ? parent.width : 0
height: 30
radius: 6
color: index === root.current ? theme.surfaceElevated
: optArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
SourceIcon {
anchors.verticalCenter: parent.verticalCenter
kind: opt.modelData.source || "local"
mono: true
}
Text {
width: parent.width - 22 - (opt.index === root.current ? 20 : 0)
height: parent.height
text: opt.modelData.label || ""
color: theme.textPrimary
font.pixelSize: theme.fontLegal
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
QbzIcon {
visible: opt.index === root.current
anchors.verticalCenter: parent.verticalCenter
name: "check"
width: 12
height: 12
tintName: "accent"
}
}
MouseArea {
id: optArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: { menu.close(); root.picked(opt.index) }
}
}
}
}
}