feat(qt): the Albums funnel survives navigation and restarts

The filter worked but forgot itself twice over: leaving Local Library
reset it, and so did closing the app. The first one is not a bug in the
view, it is what this port does — navigating away DESTROYS the view
(project_qt_nav_eager_tabs), so a `property var filter: ({})` cannot
outlive a trip to Discover.

Both follow from one move: the funnel lives on the BRIDGE, mirrored into
the shared ui_prefs.json. The bridge outlives the view, and the file
outlives the process, so one property buys both kinds of persistence.

`setFilter()` is the only writer, so a mutator cannot change the funnel
and forget to persist it, and the view re-seeds from a Connections on
the qproperty rather than only at construction — a republish has to
reach a view that is already open.

The saved funnel is pruned where the source GATES are published, not
somewhere else, so the two can never disagree: a restored `jellyfin`
tick with no Jellyfin configured would match zero rows while the chip
that clears it is hidden by the same gate — an empty grid with a badge
counting a filter the user cannot see. Only the media-server keys are
pruned; local/offline/plex are always meaningful words.

Verified: ticked Jellyfin, went to Discover and back (badge and grid
held), restarted the process (held), pressed Clear (ui_prefs went to
{}). The prune is reasoned, not exercised — both servers are configured
on this box, so no gate is off to test it against.

cc
This commit is contained in:
vicrodh committed 2026-08-20 16:38:28 -06:00
1 parent 1a2ebcf146
commit 021b5609e1
4 files changed
+113 -3

No files matched your search

+32 -3
View File
@@ -102,19 +102,48 @@ Rectangle {
// Albums quality/format/source filter (LibAlbumFilterState).
property bool filterOpen: false
property var filter: ({})
// SEEDED FROM THE BRIDGE, not defaulted to `{}`. This view is DESTROYED on
// every navigation away (see the bridge's `albums_filter` doc), so a
// view-local default meant the funnel reset the moment the user visited
// Discover — and never survived a restart at all. `QbzLocal.albumsFilter`
// outlives the view and mirrors `ui_prefs.json`.
property var filter: root.parseFilter(QbzLocal.albumsFilter)
function parseFilter(json) {
if (!json || json === "") return ({})
try { return JSON.parse(json) || ({}) } catch (e) { return ({}) }
}
// A LATER republish still wins: the gates and the saved funnel are
// published together when a media server is connected or removed, and the
// pruning that happens there (a tick whose chip is now hidden) has to
// reach a view that is already open. Guarded on a real difference so
// writing the property back does not bounce.
Connections {
target: QbzLocal
function onAlbumsFilterChanged() {
var next = root.parseFilter(QbzLocal.albumsFilter)
if (JSON.stringify(next) !== JSON.stringify(root.filter))
root.filter = next
}
}
readonly property int filterCount: {
var n = 0
for (var k in filter) if (filter[k]) n++
return n
}
/// The ONE writer. Both mutators funnel through it, so neither can change
/// the funnel and forget to persist it.
function setFilter(f) {
filter = f
QbzLocal.setAlbumsFilterJson(
Object.keys(f).length === 0 ? "" : JSON.stringify(f))
}
function toggleFilter(key) {
var f = Object.assign({}, filter)
f[key] = !f[key]
if (!f[key]) delete f[key]
filter = f
setFilter(f)
}
function clearFilter() { filter = ({}) }
function clearFilter() { setFilter({}) }
/// The funnel's state as the applied-filters tooltip wants it: three groups
/// in the popup's own order, each holding the labels of the keys that are