diff --git a/docs/requirements.md b/docs/requirements.md index b2948c52..6883976f 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -67,6 +67,7 @@ For a narrative overview of the system design, see | UR-054 | Reach account actions (Settings, Downloads, Display preferences, Sign out) from every authenticated screen via a single account menu anchored to the user's name, identical on desktop and mobile (see [ux-flows.md §1.2](ux-flows.md)) | High | Done | | UR-055 | Browse downloaded media as an offline-scoped library — reusing the same library grids, cards, and detail pages as online browsing, showing only libraries/containers with downloaded content — with the transfer-progress list demoted to a secondary "Transfers" view (see [ux-flows.md §7.2](ux-flows.md)) | High | Planned | | UR-056 | See how much disk each downloaded item/album/series consumes, in familiar rounded units shown on the card and detail page, with a device total on the Downloaded surface and a reclaim amount stated at the point of removal (see [ux-flows.md §7.3.1](ux-flows.md)) | Medium | Planned | +| UR-057 | Settings apply the instant a control is changed — no "Save" button and no save/dirty state — so leaving the page never loses a change; sliders show a live readout while dragging but persist on release (see [ux-flows.md §8.1](ux-flows.md)) | Medium | Done | --- @@ -235,6 +236,7 @@ Internal architecture, components, and application logic. | DR-083 | Downloaded browse reuses library grids, cards, and detail pages via the offline-scoped source; omits libraries/containers with no downloaded content; badges partially- vs fully-downloaded containers; play uses the local file; remove available at item/album/season/series level | UI | UR-055 | Planned | | DR-084 | Transfers view renders only in-flight rows (downloading/queued/paused/failed/waiting-for-WiFi) with Pause/Resume/Cancel/Retry; completed transfers leave the view and appear in Downloaded | UI | UR-055 | Planned | | DR-085 | Per-item on-disk size: stat downloaded files, aggregate to album/season/series subtotals and a device total, format in consistent rounded human units; surface size on cards and detail pages, the device total on the Downloaded surface, and a reclaim figure in the remove confirmation | Downloads | UR-056 | Planned | +| DR-086 | Settings page persists each control on change via per-group writers (`playerSetAudioSettings` / `playerSetVideoSettings` / `updateCacheConfig`) rather than a batch Save action; slider controls persist on `change` (pointer release) not each `input` tick; no Save button, `saving`, or `saveMessage` state | Settings | UR-057 | Done | --- @@ -300,6 +302,7 @@ Internal architecture, components, and application logic. | UR-054 | - | DR-075, DR-076, DR-077 | | UR-055 | - | DR-081, DR-082, DR-083, DR-084 | | UR-056 | - | DR-085 | +| UR-057 | - | DR-086 | --- diff --git a/docs/specs/frontend-domain-model.md b/docs/specs/frontend-domain-model.md new file mode 100644 index 00000000..7fae38e4 --- /dev/null +++ b/docs/specs/frontend-domain-model.md @@ -0,0 +1,309 @@ +# Spec: Remove Jellyfin-specific models from the frontend + +> **Implementation status (branch `frontend-domain-model`, worktree +> `../JellyTau-domain-model`):** Catalog surface **done**. The frontend's *item +> classification* and *time units* no longer speak Jellyfin: +> - `domain/` module is the single source of truth; `MediaKind` enum + isolated +> `from_jellyfin` mapping. The model gained real distinctions the flat +> `item_type` had hidden: `LiveChannel` / `ChannelItem` / `Channel`. +> - Every catalog `item.type === "..."` → `item.kind` (0 remaining in `src/`). +> - Catalog ticks → milliseconds (`durationMs`, `playbackPositionMs`); +> `formatDuration` takes ms; progress bars are unit-consistent. +> - User-facing type badge → `kindLabel()`. +> - Old Jellyfin-named fields remain **dual-carried** on the wire so nothing broke. +> +> **Deferred (tracked, not done):** +> - `primaryImageTag` → `imageId` rename (naming-only; ~40 sites across catalog + +> `PlayerMediaItem`/`MergedMediaItem`, the latter needing a Rust `image_id` +> round-trip). Catalog `MediaItem` already has `imageId`. +> - Player/session/reporting tick math (`Queue`, `SessionCard`, `RemoteControls`, +> `playbackReporting`, `playerEvents`) — crosses storage/Jellyfin *command +> signatures* in ticks; needs those commands to accept ms (phase 4). +> - `stream.type` (`mediaStreams[].type`) — Jellyfin stream vocabulary (phase 4). +> - Delete `playbackUnits.ts` / `jellyfinFieldMapping.ts` once their last +> consumers migrate; drop the dual-carried fields once nothing reads them. + +**Status:** Partially implemented (catalog surface); see banner. +**Requirements:** Architectural (boundary integrity — CLAUDE.md core principles). +Allocate new DRs on acceptance; suggested: DR for the domain `MediaItem`/`MediaKind` +type, DR for tick/image-tag hoisting, DR for the phased frontend migration +(see [requirements.md](../requirements.md)). Relates to UR-007, UR-008, UR-034. +**UX spec:** n/a — zero user-visible behaviour change. This is a pure +architecture/boundary migration. +**Supersedes / revises:** none. Extends the boundary work started in +[scoped-search-boundary.md](scoped-search-boundary.md) from *taxonomy* to the +*whole media model*. + +## Summary + +The frontend currently consumes Jellyfin's data model directly: `MediaItem` is a +Jellyfin DTO (`runTimeTicks`, `primaryImageTag`, `parentIndexNumber`, a +stringly-typed `type: string` carrying Jellyfin's item vocabulary), mirrored via +specta into **36+ frontend files**, with **127 `item.type === "…"` string +comparisons across 23 files** and two frontend utility modules +(`playbackUnits.ts`, `jellyfinFieldMapping.ts`) doing Jellyfin-specific unit and +field conversion in the presentation layer. + +This spec defines a **provider-neutral domain model**, owned by Rust, that the +Jellyfin repository maps *into*. The frontend consumes only that model. When done, +no Jellyfin vocabulary — item-type strings, ticks, image tags, Jellyfin field +names — remains in `src/`. + +## Motivation + +Two concrete problems, one strategic: + +1. **Boundary violation at scale.** Per CLAUDE.md, the frontend is + presentation-only and Rust owns the domain. Today the *domain model itself* is + Jellyfin's wire shape, propagated unchanged across IPC. The frontend knows what + a "tick" is, what `primaryImageTag` means, and that `"Audio"` is a track. That + is domain knowledge in the wrong layer, 36 files deep. +2. **Fragility.** `type: string` is unchecked: a typo (`"Epis0de"`) or a Jellyfin + rename fails silently at runtime with no compiler help, across 127 sites. Tick + math (`* 10_000_000`) duplicated frontend-side is a class of bug the backend + should have already resolved. +3. **Strategic (the reason we chose the ambitious target):** a neutral domain + model is the precondition for **ever supporting a non-Jellyfin backend** (Plex, + local files, Subsonic). As long as the UI speaks Jellyfin, that door is welded + shut. + +## Layer assignment + +| Logic / responsibility | Layer | Why it belongs there | +|------------------------|-------|----------------------| +| Definition of the media domain model (`MediaItem`, `MediaKind`) | **Rust** | The canonical shape the whole app reasons about; must not be a provider's wire format. | +| Jellyfin DTO → domain mapping (ticks→ms, image tag→url/id, `"Audio"`→`Track`, `PremiereDate`→`releaseDate`) | **Rust**, in the Jellyfin repository | Provider-specific translation; changes if Jellyfin changes; is the definition of "how Jellyfin maps to our domain." | +| Tick arithmetic (`playbackUnits.ts`) | **Rust** | A Jellyfin unit. The frontend should never see ticks; it receives `durationMs`/`positionMs`. | +| Sort-field mapping (`jellyfinFieldMapping.ts`, `title→SortName`) | **Rust** | Maps neutral sort keys to Jellyfin query fields — provider vocabulary. Frontend sends a neutral `SortKey`. | +| `MediaKind` classification (is this a track / album / episode?) | **Rust** | Derived from Jellyfin's `item_type`; the frontend receives the already-classified kind. | +| Choosing which kind renders as a card vs a list row; grid/list toggle; group order | **Frontend** | Pure presentation over the neutral `kind`. Changes only if the UI is redesigned. | +| Navigation decisions (`kind === Track && albumId` → go to album) | **Frontend** | Presentation/routing over neutral fields. | + +**Borderline calls, resolved:** + +- *`MergedMediaItem`* (the lightweight now-playing projection) is already + half-neutral (`title`, `artist`, `duration`) — it becomes a straightforward + subset of the new domain model, not a special case. +- *Context discriminators* `"album"`, `"playlist"`, `"remote"` (in `TrackList`, + playback context, sessions) are **already domain-neutral** — they are *our* + vocabulary, not Jellyfin's. They stay as-is; do not confuse them with + `item_type`. Only the Jellyfin item-type strings move. +- *`mediaStreams[].type === "Audio"/"Subtitle"/"Video"`* (track selection in + VideoPlayer) is Jellyfin stream vocabulary too, but is lower-risk and + self-contained — deferred to a late phase, not phase 1. + +## Design + +### Single canonical model, one location, isolated mappings + +The domain model is defined **once**, in a dedicated top-level Rust module +`src-tauri/src/domain/`, and is the single source of truth shared across the +whole app: + +``` +src-tauri/src/domain/ + media.rs canonical MediaItem, MediaKind, and the other media types + from_jellyfin.rs Jellyfin DTO -> domain mapping, ISOLATED here + mod.rs re-exports + | tauri-specta (export_typescript_bindings test) + v + src/lib/api/bindings.ts generated MediaItem/MediaKind — the frontend copy +``` + +- **One definition.** `domain::MediaItem` is *the* model. Rust (repositories, + player, downloads) uses it directly. The frontend uses the generated `bindings.ts` + projection of it. There is no second hand-written copy in either language, so it + cannot drift — "shared between frontend and backend" is realized by generation, + not duplication. +- **Mappings live beside the model, never in consumers.** All provider translation + (`JellyfinItem` → `domain::MediaItem`, ticks→ms, image-tag→id, item-type→`MediaKind`) + lives in `domain/from_jellyfin.rs`. It is the *only* place Jellyfin vocabulary + touches the domain type. Adding a second provider later means a new + `from_.rs` beside it — the model and every consumer stay untouched. +- **`domain` is a top-level module** (not under `repository/`) because `MediaItem` + is used by `player/`, `download/`, and `playback_mode/` too — it is not + repository-specific. +- The existing `JellyfinItem` DTO + `to_media_item()` in + [online.rs](../../src-tauri/src/repository/online.rs) is the seam that already + exists; it **moves** into `domain/from_jellyfin.rs` and is enriched to do real + translation instead of copying `item_type` through. + +### The domain model (Rust) + +```rust +// src-tauri/src/domain/media.rs — provider-neutral. NO Jellyfin vocabulary. +#[derive(specta::Type, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum MediaKind { + Track, Album, Artist, Playlist, // music + Movie, Series, Season, Episode, // video + Person, // cast/crew + Channel, Folder, // containers/live +} + +#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MediaItem { + pub id: String, + pub name: String, + pub kind: MediaKind, // was: type: String + pub is_folder: bool, + pub server_id: String, + + // Times in milliseconds — NEVER ticks. + pub duration_ms: Option, // was: run_time_ticks + + // Image as a resolved identifier the frontend turns into a URL via the + // existing image command — no raw Jellyfin tag semantics leak. + pub image_id: Option, // was: primary_image_tag + pub backdrop_image_ids: Option>, + + pub overview: Option, + pub genres: Option>, + pub production_year: Option, + pub release_date: Option, // was: premiere_date (ISO-8601) + pub community_rating: Option, + pub official_rating: Option, + + // Relationships — already neutral, kept. + pub album_id: Option, pub album_name: Option, + pub album_artist: Option, pub artists: Option>, + pub artist_items: Option>, + pub series_id: Option, pub series_name: Option, + pub season_id: Option, pub season_name: Option, + + // Ordinal position — rename off Jellyfin's index vocabulary. + pub track_number: Option, // was: index_number + pub disc_number: Option, // was: parent_index_number + + pub user_data: Option, + pub media_streams: Option>, + pub media_sources: Option>, + pub people: Option>, +} +``` + +The existing `JellyfinItem` DTO (already defined in `online.rs`, deserialized +from the Jellyfin JSON) **moves into `domain/from_jellyfin.rs`** and stays +private to that module. Its `to_media_item()` — today a near-passthrough that +copies `item_type` straight across — is enriched into the single, tested place +that: + +- classifies `item_type: String` → `MediaKind` (including the edge cases found in + the audit: `"ChannelFolderItem"` → `Channel`/`Folder` by `is_folder`, + `"TvChannel"` → `Channel`, `"Composer"/"Director"/"Writer"` → `Person`, + `"Video"` → `Movie` or a video leaf). Unknown strings map to `Folder` or a new + `Other` variant — **decide at implementation; must not panic.** +- converts `run_time_ticks` → `duration_ms` (`ticks / 10_000`). +- maps `PremiereDate` → `release_date`, image tags → image ids. + +`SortKey` enum + its Jellyfin field mapping (`jellyfinFieldMapping.ts` contents) +moves into the Jellyfin repository; the command takes a neutral `SortKey`. + +### 🔴 The `search-event` / dual-payload rule applies again + +Every path that returns `MediaItem` — command returns **and** the `search-event` +and any other event payloads — emits the new domain shape. Both sides of a +twice-delivered result must match (same rule as +[scoped-search-boundary.md](scoped-search-boundary.md)). Grep for `MediaItem` in +event definitions before declaring a phase done. + +### Frontend after + +- `MediaItem`/`MediaKind` come from generated `bindings.ts`. +- `item.type === "Audio"` → `item.kind === "track"` (127 sites, mechanical). +- `runTimeTicks` usages → `durationMs`; **delete `playbackUnits.ts`** (ticks no + longer cross the boundary; keep only any purely-display seconds↔clock helpers if + they exist, which are not Jellyfin-specific). +- `primaryImageTag` → `imageId` through the existing image-URL command. +- **Delete `jellyfinFieldMapping.ts`**; sort options send a neutral `SortKey`. +- Assert with the boundary tripwire + a new grep (see acceptance). + +## Phased migration + +This is too large and too collision-prone for one change. Phases are independently +shippable, each keeps all tests green, and each is a reviewable PR: + +1. **Establish the `domain/` module + enriched mapping, tests — no frontend + change yet.** Create `src-tauri/src/domain/{media,from_jellyfin,mod}.rs`. Move + `JellyfinItem`/`to_media_item` in. Add `MediaKind` and the neutral fields to + `domain::MediaItem` as *additive, defaulted* fields, and populate them in the + mapping, while **keeping the old Jellyfin-named fields too** (dual-carry). The + wire shape is a superset of today's, so the frontend still compiles and + behaves identically. Lands the authority + full mapping unit coverage first, + with zero blast radius on the 52 construction sites (they set the old fields; + new ones default). +2. **Flip the wire shape.** Commands + events emit the new `MediaItem`. + Regenerate `bindings.ts`. Frontend breaks to compile errors — fix them + mechanically (`type`→`kind`, values `"Audio"`→`"track"`, `runTimeTicks`→ + `durationMs`, `primaryImageTag`→`imageId`). This is the big mechanical PR; + `bun run check` is the driver. +3. **Delete the frontend conversion helpers** (`playbackUnits.ts` ticks, + `jellyfinFieldMapping.ts`) and route sorting through the neutral `SortKey`. +4. **Stream vocabulary** (`mediaStreams[].type`) and any remaining stragglers; + tighten the boundary check to forbid Jellyfin item-type strings in `src/` + outside tests. + +Ship 1 → 2 → 3 → 4 as separate PRs. Do **not** attempt all four at once. + +## Out of scope + +- Actually adding a second backend (Plex/Subsonic). This spec only *unblocks* it. +- Changing any user-visible behaviour, layout, or copy. +- The player-internal `PlayerMediaItem` / `MediaSessionType` shapes, except where + they carry the fields being renamed — align them in phase 2 only if the compiler + demands it. +- Context discriminators (`"album"`, `"playlist"`, `"remote"`) — already neutral. + +## Acceptance criteria + +- [ ] No Jellyfin item-type string (`"Audio"`, `"MusicAlbum"`, `"Series"`, …) is + compared against `.type`/`.kind` anywhere in `src/` (outside tests). Verify: + `grep -rIn '\.kind === "\(Audio\|MusicAlbum\|MusicArtist\|Series\|Episode\|Movie\|Playlist\)"' src/` returns nothing. +- [ ] No `Ticks`, `runTimeTicks`, `primaryImageTag`, `PremiereDate`, or Jellyfin + sort-field name (`SortName`, `RunTimeTicks`, …) appears in `src/` outside + tests. `playbackUnits.ts` (ticks) and `jellyfinFieldMapping.ts` are deleted. +- [ ] `MediaItem`/`MediaKind`/`SortKey` in the frontend come from `bindings.ts`. +- [ ] The `From` mapping is total and never panics on an unknown + item type (Rust test with a garbage type string). +- [ ] Behaviour is identical: same library/search/home rendering, same sorting, + same navigation, offline included. +- [ ] Both command returns and event payloads carry the new shape (no flicker). +- [ ] `bun run check`, `bun run test`, `bun run check:boundary` pass; + `cargo fmt`/`cargo clippy`/`bun run test:rust` pass; `bindings.ts` regenerated. + +## Testing + +**Rust** (`cargo test`): the `From for MediaItem` mapping is the +critical surface — +- every known `item_type` → correct `MediaKind` (table test over all 20 values + found in the audit, incl. `ChannelFolderItem`, `TvChannel`, `Composer`); +- unknown type string → safe fallback, no panic; +- `run_time_ticks` → `duration_ms` (10_000 divisor), boundary/None cases; +- `SortKey` → Jellyfin field mapping (port `jellyfinFieldMapping.ts`'s cases). + +**Frontend** (vitest): update the many tests asserting `.type`/`runTimeTicks`; +they become `.kind`/`durationMs`. `jellyfinFieldMapping`/`playbackUnits` tests are +deleted with their modules. Add a compose/render test proving `kind`-based +branching matches the old `type`-based branching for a representative mix. + +## TRACES + +Per [CLAUDE.md](../../CLAUDE.md): the domain type + mapping +`UR-007, UR-008 | `; the tick/field hoist ``; frontend migration +phases share the DRs of the capability each touches (don't invent per-file DRs). + +## Notes for the implementer + +- **This is the highest-collision change in the repo's history** — it touches 36+ + frontend files and the core Rust types. A parallel Claude session in any media + file will conflict. Strongly prefer a dedicated worktree per phase, and + `git diff` before repairing anything (CLAUDE.md gotchas / project memory). +- Phase 1 deliberately maps *back* to the old shape so it can land safely ahead of + the disruptive flip. Resist the urge to skip it. +- IPC camelCase rules apply to the new enums/structs + ([04-type-sync-and-threading.md](../architecture/04-type-sync-and-threading.md)): + `#[serde(rename_all = "camelCase")]`; tagged-enum tag convention; regenerate + `bindings.ts`, never hand-edit. +- Reviewed against [SPEC-REVIEW-CHECKLIST.md](SPEC-REVIEW-CHECKLIST.md) — the + Layer assignment table above is the load-bearing section. diff --git a/docs/traceability.md b/docs/traceability.md index 5cb0bcd4..d46c5c46 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,22 +1,22 @@ # Code Traceability Matrix -**Generated:** 7/23/2026, 7:48:39 PM +**Generated:** 7/23/2026, 8:47:53 PM ## Summary -- **Total Files Scanned:** 284 -- **Total TRACES Found:** 261 +- **Total Files Scanned:** 282 +- **Total TRACES Found:** 264 - **Requirements Covered:** - - User Requirements (UR): 52 + - User Requirements (UR): 53 - Integration Requirements (IR): 14 - - Development Requirements (DR): 75 + - Development Requirements (DR): 77 - Jellyfin API Requirements (JA): 24 ## Requirements by Type ### User Requirements (UR) ``` -UR-002, UR-003, UR-004, UR-005, UR-007, UR-008, UR-009, UR-010, UR-011, UR-012, UR-013, UR-014, UR-015, UR-016, UR-017, UR-018, UR-019, UR-020, UR-021, UR-022, UR-023, UR-024, UR-025, UR-026, UR-028, UR-029, UR-030, UR-031, UR-032, UR-033, UR-034, UR-035, UR-036, UR-038, UR-039, UR-040, UR-041, UR-042, UR-043, UR-044, UR-045, UR-046, UR-047, UR-048, UR-049, UR-050, UR-051, UR-052, UR-053, UR-054, UR-055, UR-056 +UR-002, UR-003, UR-004, UR-005, UR-007, UR-008, UR-009, UR-010, UR-011, UR-012, UR-013, UR-014, UR-015, UR-016, UR-017, UR-018, UR-019, UR-020, UR-021, UR-022, UR-023, UR-024, UR-025, UR-026, UR-028, UR-029, UR-030, UR-031, UR-032, UR-033, UR-034, UR-035, UR-036, UR-038, UR-039, UR-040, UR-041, UR-042, UR-043, UR-044, UR-045, UR-046, UR-047, UR-048, UR-049, UR-050, UR-051, UR-052, UR-053, UR-054, UR-055, UR-056, UR-057 ``` ### Integration Requirements (IR) @@ -26,7 +26,7 @@ IR-003, IR-004, IR-009, IR-010, IR-011, IR-012, IR-013, IR-014, IR-015, IR-023, ### Development Requirements (DR) ``` -DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-085 +DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086 ``` ### Jellyfin API Requirements (JA) @@ -526,10 +526,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 6 file(s) -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` - **File:** [`src/lib/utils/validation.test.ts`](src/lib/utils/validation.test.ts#L4) @@ -563,10 +563,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 3 file(s) -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) @@ -1398,8 +1398,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L43) - **Line:** 43 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2326) - - **Line:** 2326 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2330) + - **Line:** 2330 - **Context:** `Unknown` ### DR-079 @@ -1426,16 +1426,22 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-081 -**Locations:** 1 file(s) +**Locations:** 2 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) - **Line:** 10 - **Context:** `Unknown` ### DR-082 -**Locations:** 16 file(s) +**Locations:** 17 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1211) - **Line:** 1211 - **Context:** `Unknown` @@ -1475,20 +1481,23 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L640) - **Line:** 640 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) - - **Line:** 2623 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2631) + - **Line:** 2631 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) - - **Line:** 2644 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2652) + - **Line:** 2652 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) - - **Line:** 2663 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2671) + - **Line:** 2671 - **Context:** `Unknown` ### DR-083 -**Locations:** 14 file(s) +**Locations:** 16 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L795) - **Line:** 795 - **Context:** `Unknown` @@ -1501,6 +1510,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) - **Line:** 24 - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L29) + - **Line:** 29 + - **Context:** `Unknown` - **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) - **Line:** 10 - **Context:** `Unknown` @@ -1525,17 +1537,31 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L558) - **Line:** 558 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) - - **Line:** 2623 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2631) + - **Line:** 2631 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) - - **Line:** 2709 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2732) + - **Line:** 2732 + - **Context:** `Unknown` + +### DR-084 + +**Locations:** 2 file(s) + +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L315) + - **Line:** 315 - **Context:** `Unknown` ### DR-085 -**Locations:** 14 file(s) +**Locations:** 15 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1227) - **Line:** 1227 - **Context:** `Unknown` @@ -1575,8 +1601,16 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L691) - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2681) - - **Line:** 2681 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2693) + - **Line:** 2693 + - **Context:** `Unknown` + +### DR-086 + +**Locations:** 1 file(s) + +- **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) + - **Line:** 1 - **Context:** `Unknown` ### JA-001 @@ -1845,15 +1879,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1309) - **Line:** 1309 - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L394) - - **Line:** 394 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L411) - - **Line:** 411 - - **Context:** `Unknown` - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L203) - **Line:** 203 - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) + - **Line:** 434 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L451) + - **Line:** 451 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L494) - **Line:** 494 - **Context:** `Unknown` @@ -2355,10 +2389,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2098) - **Line:** 2098 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` - **File:** [`src/lib/services/preload.test.ts`](src/lib/services/preload.test.ts#L4) @@ -2419,10 +2453,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` @@ -2517,10 +2551,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 3 file(s) -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) @@ -2918,15 +2952,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1309) - **Line:** 1309 - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L394) - - **Line:** 394 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L411) - - **Line:** 411 - - **Context:** `Unknown` - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L203) - **Line:** 203 - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) + - **Line:** 434 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L451) + - **Line:** 451 + - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` @@ -3026,243 +3060,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 50 - **Context:** `export function scopeItemTypes(scope: SearchScope): string[] | undefin...` -### IT-003 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - -### IT-004 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - -### IT-013 - -**Locations:** 4 file(s) - -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) - - **Line:** 38 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - - **Line:** 51 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L569) - - **Line:** 569 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L658) - - **Line:** 658 - - **Context:** `Unknown` - -### UT-003 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - -### UT-004 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - -### UT-005 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - -### UT-025 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - - **Line:** 311 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - - **Line:** 157 - - **Context:** `pub fn file_size(&self) -> Option {` - -### UT-027 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-028 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-029 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-030 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-031 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-032 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-033 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-061 - -**Locations:** 9 file(s) - -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) - - **Line:** 38 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - - **Line:** 51 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1309) - - **Line:** 1309 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L394) - - **Line:** 394 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L411) - - **Line:** 411 - - **Context:** `Unknown` -- **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L569) - - **Line:** 569 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L658) - - **Line:** 658 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L494) - - **Line:** 494 - - **Context:** `Unknown` - -### UR-014 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/playlist.rs`](src-tauri/src/commands/playlist.rs#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UR-022 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-031 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-032 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-033 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-042 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-044 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/download/pinning.rs`](src-tauri/src/commands/download/pinning.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-045 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/download/smart_cache.rs`](src-tauri/src/commands/download/smart_cache.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-046 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - ### UR-050 **Locations:** 7 file(s) @@ -3328,8 +3125,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L43) - **Line:** 43 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2326) - - **Line:** 2326 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2330) + - **Line:** 2330 - **Context:** `Unknown` ### UR-053 @@ -3404,8 +3201,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-055 -**Locations:** 21 file(s) +**Locations:** 24 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L795) - **Line:** 795 - **Context:** `Unknown` @@ -3424,9 +3224,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) - **Line:** 24 - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L29) + - **Line:** 29 + - **Context:** `Unknown` - **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) - **Line:** 10 - **Context:** `Unknown` +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L315) + - **Line:** 315 + - **Context:** `Unknown` - **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) - **Line:** 12 - **Context:** `Unknown` @@ -3457,23 +3263,26 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L640) - **Line:** 640 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) - - **Line:** 2623 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2631) + - **Line:** 2631 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) - - **Line:** 2644 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2652) + - **Line:** 2652 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) - - **Line:** 2663 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2671) + - **Line:** 2671 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) - - **Line:** 2709 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2732) + - **Line:** 2732 - **Context:** `Unknown` ### UR-056 -**Locations:** 15 file(s) +**Locations:** 17 file(s) +- **File:** [`src/routes/downloads/+page.svelte`](src/routes/downloads/+page.svelte#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1227) - **Line:** 1227 - **Context:** `Unknown` @@ -3486,6 +3295,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L18) - **Line:** 18 - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L29) + - **Line:** 29 + - **Context:** `Unknown` - **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L17) - **Line:** 17 - **Context:** `Unknown` @@ -3516,10 +3328,75 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L691) - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2681) - - **Line:** 2681 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2693) + - **Line:** 2693 - **Context:** `Unknown` +### UR-057 + +**Locations:** 1 file(s) + +- **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### IT-003 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + +### IT-004 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + +### IT-013 + +**Locations:** 4 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) + - **Line:** 38 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) + - **Line:** 51 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L569) + - **Line:** 569 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L658) + - **Line:** 658 + - **Context:** `Unknown` + +### UT-003 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + +### UT-004 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + +### UT-005 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + ### UT-010 **Locations:** 1 file(s) @@ -3536,16 +3413,220 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 81 - **Context:** `pub fn cancel(&mut self) {` -### UT-014 +### UT-024 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` + +### UT-026 **Locations:** 2 file(s) -- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - - **Line:** 311 +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) + - **Line:** 251 + - **Context:** `Unknown` + +### UT-027 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-028 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-029 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-030 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-031 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-032 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-033 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-050 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UT-052 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L315) + - **Line:** 315 + - **Context:** `Unknown` + +### UT-060 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UT-061 + +**Locations:** 9 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) + - **Line:** 38 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) + - **Line:** 51 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1309) + - **Line:** 1309 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) + - **Line:** 434 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L451) + - **Line:** 451 + - **Context:** `Unknown` +- **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L569) + - **Line:** 569 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L658) + - **Line:** 658 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L494) + - **Line:** 494 + - **Context:** `Unknown` + +### UT-066 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UR-014 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/playlist.rs`](src-tauri/src/commands/playlist.rs#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UR-022 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-031 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-032 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-033 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-044 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/download/pinning.rs`](src-tauri/src/commands/download/pinning.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-045 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/download/smart_cache.rs`](src-tauri/src/commands/download/smart_cache.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-046 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - - **Line:** 157 - - **Context:** `pub fn file_size(&self) -> Option {` ### UT-015 @@ -3617,17 +3698,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - - **Line:** 311 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - - **Line:** 157 - - **Context:** `pub fn file_size(&self) -> Option {` - -### UT-022 - -**Locations:** 2 file(s) - - **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - **Line:** 311 - **Context:** `Unknown` @@ -3646,24 +3716,16 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 157 - **Context:** `pub fn file_size(&self) -> Option {` -### UT-024 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UT-026 +### UT-025 **Locations:** 2 file(s) -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) - - **Line:** 251 +- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) + - **Line:** 311 - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) + - **Line:** 157 + - **Context:** `pub fn file_size(&self) -> Option {` ### UT-042 @@ -3681,31 +3743,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2027 - **Context:** `Unknown` -### UT-050 +### UR-042 **Locations:** 1 file(s) -- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UT-060 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UT-066 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) - - **Line:** 4 +- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) + - **Line:** 3 - **Context:** `Unknown` ### UR-047 @@ -3716,6 +3759,28 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 3 - **Context:** `Unknown` +### UT-014 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) + - **Line:** 311 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) + - **Line:** 157 + - **Context:** `pub fn file_size(&self) -> Option {` + +### UT-022 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) + - **Line:** 311 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) + - **Line:** 157 + - **Context:** `pub fn file_size(&self) -> Option {` + ### UT-044 **Locations:** 1 file(s) @@ -3728,40 +3793,40 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) - - **Line:** 2623 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2631) + - **Line:** 2631 - **Context:** `Unknown` ### UT-047 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) - - **Line:** 2644 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2652) + - **Line:** 2652 - **Context:** `Unknown` ### UT-048 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) - - **Line:** 2663 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2671) + - **Line:** 2671 - **Context:** `Unknown` ### UT-049 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2681) - - **Line:** 2681 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2693) + - **Line:** 2693 - **Context:** `Unknown` ### UT-051 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) - - **Line:** 2709 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2732) + - **Line:** 2732 - **Context:** `Unknown` ### UT-059 @@ -3782,8 +3847,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2326) - - **Line:** 2326 +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2330) + - **Line:** 2330 - **Context:** `Unknown` diff --git a/docs/ux-flows.md b/docs/ux-flows.md index e20072ab..4cd0ada8 100644 --- a/docs/ux-flows.md +++ b/docs/ux-flows.md @@ -1076,6 +1076,13 @@ flowchart TB - **Mobile:** Click three-dot overflow menu → Select "Settings" - **Direct:** Navigate to `/settings` route +**Settings apply instantly.** Every control on the Settings page persists the +moment the user changes it — toggling a switch, picking a level, or releasing a +slider writes that setting immediately. There is **no "Save" button** and no +save/dirty state to reason about; leaving the page never risks losing a change. +Sliders update their live readout while dragging but only persist on release +(`change`, not each `input` tick) to avoid flooding the backend. + ### 8.2 Logout Flow ```mermaid diff --git a/src/lib/components/SkeletonLoader.svelte b/src/lib/components/SkeletonLoader.svelte deleted file mode 100644 index c9a70639..00000000 --- a/src/lib/components/SkeletonLoader.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - -{#if type === "card"} -
- {#each Array(count) as _, i (i)} -
-
-
-
-
-
-
- {/each} -
-{:else if type === "banner"} -
-
-
-{:else if type === "circle"} -
- {#each Array(count) as _, i (i)} -
-
-
-
- {/each} -
-{:else if type === "row"} -
- {#each Array(count) as _, i (i)} -
-
-
-
-
-
-
- {/each} -
-{:else if type === "text"} -
- {#each Array(count) as _, i (i)} -
- {/each} -
-{/if} - - diff --git a/src/lib/components/downloads/StorageManagement.svelte b/src/lib/components/downloads/StorageManagement.svelte deleted file mode 100644 index 2a0740f3..00000000 --- a/src/lib/components/downloads/StorageManagement.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - -
- -
-

Storage

- {#if stats && stats.total_items > 0} - - {/if} -
- - {#if loading} -
-
-
- {:else if stats} - -
-
- - - -
-
-

{formatBytes(stats.total_bytes)}

-

- {stats.total_items} {stats.total_items === 1 ? "item" : "items"} downloaded -

-
-
- - - {#if stats.albums.length > 0} - - - - {#if showBreakdown} -
- {#each stats.albums as album (album.album_id)} -
- -
- {formatBytes(album.bytes_used)} - -
-
- {/each} -
- {/if} - {/if} - - - {#if stats.total_items === 0} -
-

No downloads yet

-

Downloaded media will appear here

-
- {/if} - {/if} -
- - -{#if showDeleteAllConfirm} -
-
-
-
- - - -
-

Delete All Downloads?

-

- This will remove {stats?.total_items || 0} downloaded items and free up {formatBytes(stats?.total_bytes || 0)} of storage. This action cannot be undone. -

-
- - -
-
-
-
-{/if} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index ab51046d..ec04b69c 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,4 +1,4 @@ - + @@ -279,7 +321,8 @@ max="12" step="0.5" value={settings.crossfadeDuration} - oninput={handleCrossfadeChange} + oninput={handleCrossfadeInput} + onchange={handleCrossfadeChange} class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]" />
@@ -425,7 +468,8 @@ max="30" step="5" value={videoSettings.autoPlayCountdownSeconds} - oninput={handleCountdownChange} + oninput={handleCountdownInput} + onchange={handleCountdownChange} class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]" />
@@ -445,7 +489,7 @@
{#each episodeLimitOptions as option}
- -