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
167 lines
8.1 KiB
Markdown
167 lines
8.1 KiB
Markdown
# 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](../requirements.md)).
|
||
**UX spec:** [ux-flows.md §7.2–7.7](../ux-flows.md).
|
||
|
||
## Summary
|
||
|
||
Replace the flat Active/Completed download list with two views under
|
||
`/downloads`:
|
||
|
||
1. **Downloaded** (default) — the library, filtered to what's on the device,
|
||
using the *same* browse screens as online (grids, cards, detail pages).
|
||
2. **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
|
||
|
||
1. **The offline repository is already a browsable tree.**
|
||
[offline.rs](../../src-tauri/src/repository/offline.rs) — `get_items` returns
|
||
downloaded items **plus** containers (MusicAlbum, Series, Season) that have at
|
||
least one downloaded child. `get_libraries`, `get_item`, and `search` all
|
||
filter to downloaded content via CTEs. This is the data source for
|
||
Downloaded; **do not build a new query layer.**
|
||
|
||
2. **The client cannot reach it independently.**
|
||
[repository-client.ts](../../src/lib/api/repository-client.ts) `getItems` →
|
||
`repositoryGetItems` always goes through the **hybrid** repository
|
||
([hybrid.rs](../../src-tauri/src/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).
|
||
|
||
3. **Downloads page is a flat two-tab list.**
|
||
[downloads/+page.svelte](../../src/routes/downloads/+page.svelte) — Active /
|
||
Completed tabs, one `DownloadItem` row per transfer, no browsing.
|
||
|
||
4. **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.
|
||
|
||
5. **A related fallthrough bug is already tracked** (DR-080, another session):
|
||
`HybridRepository::get_items` treats 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."
|
||
|
||
6. **Concurrency, the 3-download cap, and the auto-pump are backend concerns.**
|
||
Do not surface them as manual controls; do not loop `startDownload` from the
|
||
frontend (see [CLAUDE.md](../../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
|
||
|
||
- [ ] `/downloads` opens 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 clippy` pass.
|
||
|
||
## 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](../../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](../ux-flows.md) 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](../../src/routes/downloads/+page.svelte)
|
||
and the repository layer; check `git diff` before repairing unexpected changes.
|