Files
jellytau/docs/specs/downloads-as-offline-library.md
dtourolle 8b028b6b60 docs: specs, requirements, ux-flows and traceability for new features
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
2026-07-23 20:04:35 +02:00

8.1 KiB
Raw Permalink Blame History

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.27.7.

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.rsget_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 getItemsrepositoryGetItems always 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).

  3. Downloads page is a flat two-tab list. 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 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, 23 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 (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.27.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-078080). Coordinate on downloads/+page.svelte and the repository layer; check git diff before repairing unexpected changes.