Add specs for the account menu, downloads-as-offline-library, offline downloaded-only filter, and scoped search (+ boundary revision). Add the new UR/DR entries to requirements.md, update ux-flows, and regenerate the traceability matrix. TRACES: UR-049, UR-050, UR-052, UR-053, UR-054, UR-055, UR-056
8.1 KiB
Spec: Downloads as a browsable offline library
Status: Draft — ready to implement Scope: Frontend-heavy; one new repository-client browse path. Minimal Rust. Requirements: UR-055 → DR-081, DR-082, DR-083, DR-084; UR-056 → DR-085 (see requirements.md). UX spec: ux-flows.md §7.2–7.7.
Summary
Replace the flat Active/Completed download list with two views under
/downloads:
- Downloaded (default) — the library, filtered to what's on the device, using the same browse screens as online (grids, cards, detail pages).
- Transfers — the existing progress-row list, demoted to a secondary tab, showing only in-flight transfers.
Plus per-item disk usage (UR-056) shown in familiar units on cards, detail pages, a device total, and the remove confirmation.
Motivation
A user who downloaded three seasons and two albums sees ~70 individual transfer rows today, with no grouping and no reuse of the library UI. "What do I have offline" and "what is downloading" are different questions crammed into one flat list. Browsing offline should feel exactly like browsing online.
Background: verified current state
-
The offline repository is already a browsable tree. offline.rs —
get_itemsreturns downloaded items plus containers (MusicAlbum, Series, Season) that have at least one downloaded child.get_libraries,get_item, andsearchall filter to downloaded content via CTEs. This is the data source for Downloaded; do not build a new query layer. -
The client cannot reach it independently. repository-client.ts
getItems→repositoryGetItemsalways goes through the hybrid repository (hybrid.rs), which merges cache and server. There is no "offline only" browse path exposed. This is the one real backend gap (DR-082). -
Downloads page is a flat two-tab list. downloads/+page.svelte — Active / Completed tabs, one
DownloadItemrow per transfer, no browsing. -
Library browse components are reusable as-is.
LibraryGrid,MediaCard, the/library/[id]detail page (§5A/§5B) render whatever items they are given. Downloaded browse is those components with an offline-scoped source. -
A related fallthrough bug is already tracked (DR-080, another session):
HybridRepository::get_itemstreats an empty offline result as a cache miss and falls through to the server. The offline-only browse path (DR-082) must not share that behaviour — an empty result there is authoritative "nothing downloaded here." -
Concurrency, the 3-download cap, and the auto-pump are backend concerns. Do not surface them as manual controls; do not loop
startDownloadfrom the frontend (see CLAUDE.md gotchas).
Design
View split (DR-081)
/downloads renders a Downloaded / Transfers switch. Downloaded is the
default. Transfers shows a count/badge only while transfers are active.
Initiating downloads stays on item/album/series detail pages (§7.1) — this page
does not start downloads.
Offline-scoped browse source (DR-082, DR-083)
Add an explicit offline-only browse path so Downloaded never merges server results and never depends on reachability. Two viable shapes — pick per the codebase, do not do both:
- (a) A dedicated command (e.g.
repository_get_downloaded_items/_libraries) that calls the offline repository directly, with a matching client method; or - (b) An explicit
offlineOnly/scope flag on the existing get-items path that bypasses the hybrid merge and the empty→fallthrough behaviour.
Either way: an empty result is authoritative (do not reuse the DR-080 fallthrough), and the path is available while the server is reachable (a user online still wants to browse their downloads).
Downloaded then reuses LibraryGrid / MediaCard / the detail page against this
source. Omit libraries and containers with no downloaded content. Badge
partially- vs fully-downloaded containers. Play uses the local file; remove is
available at item / album / season / series level and removes a container from
the browse when its last downloaded child goes.
Transfers view (DR-084)
The existing list, filtered to in-flight rows only: downloading (with progress), queued, paused, failed, waiting-for-WiFi (the DR-074 state from the other session). Controls: Pause / Resume / Cancel / Retry. Completed transfers leave this view — they appear in Downloaded. Empty state points at the library.
Disk usage (DR-085, UR-056)
- Source the bytes from the download manager — it writes the files and can stat them. Aggregate to album/season/series subtotals and a device total. This is display + aggregation, not new tracking.
- Format once, consistently. One shared formatter, human units, 2–3
significant figures (
1.2 GB,340 MB). Binary vs decimal — pick one and use it everywhere. - Surface it in familiar places: a secondary size label on the card and
detail page; a device total at the top of Downloaded (
3.4 GB · 12 items) that reconciles with the listed sum; a reclaim figure in the remove confirmation ("frees 1.2 GB"). No separate "storage report" screen. - Sort/filter by size is a nice-to-have, not required for v1.
Out of scope
- Changing download initiation, the 3-concurrent cap, or the auto-pump.
- The catalog-browse / show-server-catalog toggle (UR-052, another session) — that governs the online offline-fallback library; this is the dedicated Downloads surface. They should be consistent but are separate work.
- Fixing the DR-080 hybrid fallthrough bug (owned elsewhere) — just don't depend on that behaviour here.
Acceptance criteria
/downloadsopens on Downloaded and can switch to Transfers.- Downloaded lists only libraries/containers with downloaded content, using the same grids/cards/detail pages as online browsing.
- Browsing Downloaded never shows non-downloaded server items, online or off.
- An empty Downloaded result reads as "nothing downloaded," never falls through to the server.
- Play from Downloaded plays the local file.
- Remove works at item/album/season/series level and updates the browse.
- Transfers shows only in-flight rows with working controls; finished transfers move to Downloaded.
- Each downloaded item/container shows its on-disk size; a device total is shown and reconciles with the sum; remove states the reclaim amount.
bun run check,bun run test, and (if Rust touched)cargo test+cargo clippypass.
Testing
- Repository client: the offline-only browse path returns downloaded content and its containers, and an empty result does not trigger server fallthrough.
- Downloaded view: libraries/containers with no downloads are omitted; partial/full container badging.
- Transfers: only in-flight statuses render; a completed transfer disappears.
- Size formatter: rounding and unit thresholds; subtotal aggregation; device total reconciles with listed items.
- If a Rust command is added, add the tauri IPC param-naming coverage per CLAUDE.md (camelCase rule).
New requirement-implementing code needs TRACES: comments. Suggested tags:
view split UR-055 | DR-081; offline browse path UR-055 | DR-082, DR-083;
Transfers UR-055 | DR-084; size display UR-056 | DR-085.
Notes for the implementer
- Read ux-flows.md §7.2–7.7 first — behavioural spec; this is the implementation plan.
- The offline repository already does the hard part. The main work is a clean offline-only client path and reusing the library components — resist rebuilding browse UI.
- Another session is active in downloads/offline/connectivity code (DR-074,
DR-078–080). Coordinate on downloads/+page.svelte
and the repository layer; check
git diffbefore repairing unexpected changes.