feat(qbz-qt): Local Library complete — five tabs, routed album page, Plex

The view had roughly a folder tree and an album grid inside one 1,847-line
file. Slint's is 2,671 lines across five tabs, and the local album detail is a
routed page of its own. This closes that gap and splits the result: the view
becomes a composition root over qml/views/local/ modules, and
local_library_qt.rs becomes a narrow facade over local_* modules by concern.

Plex is wired for real. The port hardcoded the cache path to None, so the union
was local-only and Plex thumbnails were skipped. The settings store binds per
user at session activation, the album and track union includes Plex, thumbnails
resolve through the artwork path, rows carry their source badge, and the manual
re-sync is on the toolbar behind the availability gate — as in Slint.

Tabs: Albums with its quality/format/source filter popup, Tracks paginated,
Folders in both flat and two-pane tree modes, Artists as an A-Z master/detail.
Every list is windowed from the start, artwork id-keyed and evicted, because
16k tracks is exactly where the Slint build freezes.

The tree grows a real select mode with its bulk bar, the ad-hoc "ephemeral
folder" pane plays music that was never indexed, and the album page gets its
version picker — a local album with two physical copies now switches between
them without a database round-trip.

Two agents built the QML and the Rust halves in parallel and the QML called
thirty QbzLocal members that did not exist. QML resolves lazily, so each would
have been a control that renders and does nothing; a static cross-reference
caught them before a build ever ran, and they are implemented here.

Deliberately logged seams, not half-paths: add-to-playlist, add-to-mixtape and
the tag editor have no picker or modal in this port yet, and local favourites
are not wired, so those actions log rather than route a local id into the Qobuz
API. Plex playback downloads the part instead of range-streaming it, which is
Slint's own documented fallback.

cc
This commit is contained in:
vicrodh committed 2026-07-28 15:25:00 -06:00
1 parent ade156a72a
commit 31b1484c01
47 files changed
+8116 -2968

No files matched your search

@@ -0,0 +1,84 @@
// Local track row — the SHARED rows/TrackRow.qml with the local arms set,
// plus the two things the shared row has no arm for:
//
// * the always-visible SOURCE glyph the Slint renders from
// `show-source: true` (LocalLibraryView.slint:1489) — drawn OUTSIDE the
// shared row so TrackRow keeps one column layout for every surface;
// * the multi-select checkbox (Slint TrackRow's `multi-select-mode`
// swaps the number cell for it) — overlaid on the number cell here for
// the same reason: no fork of the shared row, no shared-file edit.
//
// Artwork follows the Slint gate (AppearanceState.local-library-track-
// artwork, default OFF) — decoding a cover per row is exactly what froze
// this surface at 16k tracks.
import QtQuick
import com.blitzfc.qbz
import "../../rows"
import "../../theme"
Item {
id: root
property var item: ({})
property int number: 0
property bool showAlbum: true
property bool showArtwork: false
property string artSource: ""
property bool selectMode: false
property bool checked: false
signal playRequested()
signal enqueueRequested(string mode)
signal toggleSelect()
QbzTheme { id: theme }
height: 50
TrackRow {
id: sharedRow
width: root.width - 26
item: root.item
number: root.number
showArtwork: root.showArtwork
showAlbum: root.showAlbum
showFavorite: false
showDownload: false
menuShowGoTo: false
menuShowFavorite: false
// Local rows are NOT drag sources: item.id is a local DB row id and
// the sidebar drop handler forwards it as a Qobuz catalog id.
draggable: false
qualityStyle: "icon"
onPlayRequested: root.playRequested()
onEnqueueRequested: function (m) { root.enqueueRequested(m) }
}
// Multi-select checkbox over the number cell (x 12..44 in TrackRow).
Rectangle {
visible: root.selectMode
x: 12
width: 32
height: 40
anchors.verticalCenter: parent.verticalCenter
color: theme.surfaceMain
radius: 4
SelectCheck {
anchors.centerIn: parent
on: root.checked
onToggled: root.toggleSelect()
}
}
// Source glyph (local / offline cache / Plex).
QbzIcon {
visible: root.item.source === "local" || root.item.source === "plex"
|| root.item.source === "offline"
name: root.item.source === "offline" ? "cloud-download" : "hard-drive"
width: 14
height: 14
x: root.width - 20
anchors.verticalCenter: parent.verticalCenter
tintName: root.item.source === "plex" ? "accent" : "muted"
}
}