From 8b028b6b6017ea65ab870018c6cc8645d3b7f75f Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 23 Jul 2026 20:04:35 +0200 Subject: [PATCH] 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 --- docs/architecture/02-svelte-frontend.md | 28 +- docs/requirements.md | 48 + docs/specs/SPEC-REVIEW-CHECKLIST.md | 68 + docs/specs/SPEC-TEMPLATE.md | 105 ++ docs/specs/account-menu.md | 164 ++ docs/specs/downloads-as-offline-library.md | 166 ++ docs/specs/offline-downloaded-only-filter.md | 235 +++ docs/specs/scoped-search-boundary.md | 273 +++ docs/specs/scoped-search.md | 202 ++ docs/traceability-ci.md | 16 +- docs/traceability.md | 1764 +++++++++++++----- docs/ux-flows.md | 627 ++++++- 12 files changed, 3121 insertions(+), 575 deletions(-) create mode 100644 docs/specs/SPEC-REVIEW-CHECKLIST.md create mode 100644 docs/specs/SPEC-TEMPLATE.md create mode 100644 docs/specs/account-menu.md create mode 100644 docs/specs/downloads-as-offline-library.md create mode 100644 docs/specs/offline-downloaded-only-filter.md create mode 100644 docs/specs/scoped-search-boundary.md create mode 100644 docs/specs/scoped-search.md diff --git a/docs/architecture/02-svelte-frontend.md b/docs/architecture/02-svelte-frontend.md index 757b84f6..a423eb54 100644 --- a/docs/architecture/02-svelte-frontend.md +++ b/docs/architecture/02-svelte-frontend.md @@ -51,14 +51,19 @@ graph TD **View Enforcement:** +Ordinal content (where position carries meaning) is always a list. Everything +else honours the user's persisted grid/list preference — see +[ux-flows.md §5A.2](../ux-flows.md). + | Content Type | View Mode | Toggle Visible | Component Used | |--------------|-----------|----------------|----------------| -| Tracks | List (forced) | No | `TrackList` | -| Artists | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` | -| Albums | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` | -| Playlists | Grid (forced) | No | `LibraryGrid` with `forceGrid={true}` | -| Genres | Grid (both levels) | No | `LibraryGrid` with `forceGrid={true}` | -| Album Detail Tracks | List (forced) | No | `TrackList` | +| Tracks | List (forced — ordinal) | No | `TrackList` | +| Artists | User preference | Yes | `LibraryGrid` | +| Albums | User preference | Yes | `LibraryGrid` | +| Playlists | User preference | Yes | `LibraryGrid` | +| Genres | User preference (both levels) | Yes | `LibraryGrid` | +| Album Detail Tracks | List (forced — ordinal) | No | `TrackList` | +| Season Episodes | List (forced — ordinal) | No | `SeasonSection` | **TrackList Component:** @@ -80,9 +85,16 @@ The `TrackList` component (`src/lib/components/library/TrackList.svelte`) is a d /> ``` -**LibraryGrid forceGrid Prop:** +**LibraryGrid view mode:** -The `forceGrid` prop prevents the grid/list view toggle from appearing and forces grid view regardless of user preference. This ensures visual content (artists, albums, playlists) is always displayed as cards with artwork. +`LibraryGrid` reads the global `viewMode` store (persisted to `localStorage`) +and renders `LibraryListView` or the card grid accordingly. The `showViewToggle` +prop controls whether the toggle buttons appear in the page header; the grid +itself always follows the stored preference. + +A `forceGrid` prop previously existed to pin pages to grid regardless of +preference. No caller ever passed it, so it was removed — pages that were +documented as "forced grid" have in practice always honoured the toggle. ## Playback Reporting Service diff --git a/docs/requirements.md b/docs/requirements.md index f350ed7e..b2948c52 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -58,6 +58,15 @@ For a narrative overview of the system design, see | UR-045 | Predictively pre-cache likely-next media (queue lookahead and album affinity) within a storage budget | Low | Done | | UR-046 | Group multiple remote players into a synchronized playback group (LMS SyncGroups) | Low | Done | | UR-047 | Manage multiple Jellyfin servers (add, list, remove) and switch the active server/account | Medium | Planned (backend store done; switcher UI pending) | +| UR-048 | See the next episodes of a series directly below the episode/series being viewed, above cast and similar-shows content, so continuing a show is the shortest path (see [ux-flows.md §5B](ux-flows.md)) | High | Done | +| UR-049 | Search is scoped by where it was started — inside a library it searches that library, from Home/library-root/search-tab it searches everything — with the scope shown as filter chips under the search bar that preselect from context and can be changed without retyping (see [ux-flows.md §6.1](ux-flows.md)) | High | Implemented | +| UR-050 | Reorder search result groups (Songs, Albums, Artists, Movies, TV Shows) by drag and drop in settings, so the media a user cares about most appears first (see [ux-flows.md §6.3](ux-flows.md)) | Medium | Implemented | +| UR-051 | Browse library pages in a consistent layout where card shape signals media type (square music, poster video, thumbnail episode), ordinal content stays listed, and the grid/list preference persists across pages (see [ux-flows.md §5A](ux-flows.md)) | Medium | Partial (implemented; toggle not reachable from settings) | +| UR-052 | While offline, library pages show only media available on the device by default, with an opt-in toggle that additionally reveals the cached server catalog as greyed-out entries which can be queued for download on the next reconnect | High | Broken (toggle does not gate the listing; see issue #10) | +| UR-053 | Restrict media downloads to unmetered networks via a "WiFi Only" setting: when enabled, queued downloads are held while the device is on cellular or a metered connection (including metered WiFi hotspots) and resume automatically once an unmetered network is available | Medium | Done (pending device verification) | +| 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 | --- @@ -97,6 +106,7 @@ External system integrations and platform-specific implementations. | IR-026 | Android picture-in-picture: auto-enter on user-leave-hint via `enterPictureInPictureMode`, **only while a local video surface is actively rendering** (never for audio-only playback, menu/library browsing, or remote/cast sessions — enforced by the native `canEnterPip` guard, re-checked at leave time); aspect-ratio sizing; a play/pause RemoteAction that **reflects live player play/pause state** (updated whenever playback state changes, not only on button press); WebView hide/restore on mode change | Platform | UR-041 | Done | | IR-027 | Jellyfin `/System/Info/Public` reachability probe used as an offline→online recovery detector | API | UR-043 | Done | | IR-028 | Jellyfin/LMS SyncGroups API client (list, create, join, unsync, dissolve sync groups) | API | UR-046 | Done | +| IR-029 | Android `ConnectivityManager`/`NetworkCapabilities` transport probe with a `NetworkCallback` change subscription, surfaced to the frontend via the `AndroidNetworkType` JS bridge and the `jellytau-network-changed` WebView event (requires `ACCESS_NETWORK_STATE`) | Platform | UR-053 | Done (pending device verification) | ### 2.2 Jellyfin API Requirements @@ -203,6 +213,28 @@ Internal architecture, components, and application logic. | DR-058 | Remote sync-group control (LMS SyncGroups): list, create, unsync a player, dissolve a group | Player | UR-046 | Done | | DR-059 | Playback-mode transfer state machine: get/set current mode, transferring guard, transfer-to-remote / transfer-to-local, remote session status | Player | UR-010 | Done | | DR-060 | Multi-server store and active-account selection: save/get/delete server, save/get user, set/get active user (per-server), active-session resolution | Storage | UR-047 | Partial (store done; server-switcher UI pending) | +| DR-061 | Episode Focus View: episode hero followed *immediately* by the "More Episodes" strip — a forward-biased window (~3 before / ~6 after) around the current episode, spanning season boundaries in series order, with the current episode present and badged, per-card resume progress and watched state, and click-to-swap focus (no playback) | UI | UR-048 | Done | +| DR-062 | Detail-page section ordering: continuation content precedes discovery content — Episode Focus View renders hero → episode strip → cast → similar; Series renders hero → seasons/episodes → cast → similar | UI | UR-048 | Done | +| DR-063 | Search scope resolver mapping the originating route to an `includeItemTypes` set (All / Music / Movies / TV), defaulting to All for Home, `/library`, and the search tab | UI | UR-049 | Implemented | +| DR-064 | Scope chip row rendered under the search bar on both the search page and the in-library header search: preselected from context, horizontally scrollable, re-runs the search preserving the query on change | UI | UR-049 | Implemented | +| DR-065 | Thread `SearchOptions.includeItemTypes` through `library.search()` so the global/header search honours scope (backend online + offline paths already support it) | UI | UR-049 | Implemented | +| DR-066 | Persisted search result group order with a drag-and-drop settings list, keyboard-accessible reordering, a shipped default (Songs → Albums → Artists → Movies → TV Shows), and empty-group omission | Settings | UR-050 | Implemented | +| DR-067 | `SearchResults` renders groups in the user-configured order rather than hardcoded markup order, without altering intra-group ranking | UI | UR-050 | Implemented | +| DR-068 | Library card shape by media type: 1:1 square for music (circular mask for artists), 2:3 poster for movies/series/seasons, 16:9 for episodes and collection folders | UI | UR-051 | Done | +| DR-069 | Responsive library grid (2/3/4/5/6 columns across base→xl) with two-line truncated card text and artwork-overlay progress/watched state | UI | UR-051 | Done | +| DR-070 | Global persisted grid/list view preference honoured by browse pages, suppressed for ordinal content (album tracks, season episodes) | UI | UR-051, UR-029 | Partial (persisted store + page-header toggle; no settings entry) | +| DR-075 | Shared `AccountMenu` component: identity header (user + server), Downloads / Settings / Display entries, divider, Sign out last; anchored to the username/avatar trigger and identical on desktop and mobile | UI | UR-054 | Done | +| DR-076 | App shell exposes the header (and therefore the account menu) on every authenticated non-immersive route, including `/`, `/search`, and `/downloads`; only `/player/*` and `/login` remain chrome-free | UI | UR-054 | Done | +| DR-077 | Display section in Settings binding the existing persisted grid/list `viewMode` store, giving the preference a discoverable home | Settings | UR-054, UR-029 | Done | +| DR-078 | Catalog-visibility gate spanning the "Show all server media" toggle → `set_show_server_catalog` → `INCLUDE_CATALOG_BROWSE` → the synced-catalog UNION branch of offline `get_items`. Visibility resolves to `serverReachable \|\| showServerCatalog`, so offline with the toggle off lists downloaded/local media only | Storage | UR-052, UR-002 | Partial (gate implemented and unit-tested; defeated upstream by DR-079 and by the repository fallback in DR-080) | +| DR-079 | `isConnected` derives from backend-reported server reachability alone; `navigator.onLine` is advisory and may only trigger a recheck, never force or clear the offline state (a reachable LAN server while the browser reports offline, and an unreachable server on a live link, must both resolve correctly) | Connectivity | UR-052, UR-043 | Broken (`isConnected` ANDs in `navigator.onLine`, so a live link with an unreachable server never enters offline listing) | +| DR-080 | With the catalog-browse gate off, an empty offline `get_items` result is authoritative "no downloads here" and must be returned as-is; the hybrid repository must not treat it as a cache miss and fall through to the server | Storage | UR-052, UR-013 | Broken (`has_content()` cache-hit test in `HybridRepository::get_items`/`parallel_race` falls through to the server on an intentionally empty result) | +| DR-074 | WiFi-only download gate: `NetworkState`/`NetworkType` transport model reported from the platform via `set_network_state`, checked in `pump_download_queue` before starting any pending row (cellular/metered/unknown fail closed, WiFi and Ethernet require `NOT_METERED`); blocked rows stay `pending` and re-pump on network change, with a `waitingForNetwork` event driving the "Waiting for WiFi" notice. Also wires the previously inert Smart Caching / Queue Pre-caching / WiFi Only settings toggles to `CacheConfig` | Downloads | UR-053 | Done (pending device verification) | +| DR-081 | `/downloads` split into a default **Downloaded** browse view and a secondary **Transfers** activity view, with a view switch and a Transfers badge shown only while transfers are active | UI | UR-055 | Planned | +| DR-082 | Offline-scoped browse entry point in the repository client: browse downloaded content only (offline repository `get_items`/`get_libraries` — downloaded items plus their containers) independent of server reachability, without merging server catalog | Storage | UR-055 | Planned | +| 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 | --- @@ -259,6 +291,15 @@ Internal architecture, components, and application logic. | UR-045 | - | DR-057 | | UR-046 | IR-028 | DR-058 | | UR-047 | IR-013 | DR-060 | +| UR-048 | - | DR-061, DR-062 | +| UR-049 | IR-010 | DR-063, DR-064, DR-065 | +| UR-050 | - | DR-066, DR-067 | +| UR-051 | - | DR-068, DR-069, DR-070 | +| UR-052 | IR-027 | DR-078, DR-079, DR-080 | +| UR-053 | IR-029 | DR-074 | +| UR-054 | - | DR-075, DR-076, DR-077 | +| UR-055 | - | DR-081, DR-082, DR-083, DR-084 | +| UR-056 | - | DR-085 | --- @@ -329,6 +370,11 @@ Internal architecture, components, and application logic. | UT-059 | Audio-only stream URL builder for a video item (selected audio-stream index) | JA-032, DR-052 | Pending | | UT-060 | Background-audio handoff state machine (background→audio, foreground→video; no dual audio) | DR-052 | Pending | | UT-061 | Background-audio Tauri command param naming (camelCase) | DR-052 | Pending | +| UT-067 | Offline `get_items` gates the synced-catalog UNION on the catalog-browse flag (downloads only when off, full catalog when on) | DR-078 | Done | +| UT-068 | Catalog visibility resolves to `serverReachable \|\| showServerCatalog`, and is pushed to the backend on every change of either input | DR-078, DR-079 | Pending | +| UT-069 | `isConnected` follows backend reachability alone: false when the server is unreachable on a live link, true for a reachable server while `navigator.onLine` is false | DR-079 | Pending | +| UT-070 | Hybrid `get_items` returns an empty offline result as-is when the catalog-browse gate is off, without querying the server | DR-080 | Pending | +| UT-066 | WiFi-only download gate: cellular and metered WiFi blocked, unmetered WiFi/Ethernet allowed, unknown/none fail closed, desktop default ungated; plus the frontend network reporter (transport reporting, change subscription, teardown, fail-open queries) | DR-074 | Done | ### Integration Tests @@ -347,6 +393,8 @@ Internal architecture, components, and application logic. | IT-011 | Resume playback from server position | IR-015, UR-019 | Pending | | IT-012 | Equalizer bands via libmpv | IR-020, UR-027 | Pending | | IT-013 | Background-audio handoff on Android: background/lock continues audio via native service and stops video decode; foreground resumes video at position | IR-025, UR-040 | Pending | +| IT-016 | Offline library listing end-to-end: with the server unreachable, a library page lists only downloaded media with the toggle off, and additionally reveals greyed-out cached catalog entries with the toggle on | UR-052, DR-078, DR-079, DR-080 | Pending | +| IT-017 | A download queued from a greyed-out offline catalog entry persists and is resolved and started on reconnect | UR-052, UR-011 | Pending | --- diff --git a/docs/specs/SPEC-REVIEW-CHECKLIST.md b/docs/specs/SPEC-REVIEW-CHECKLIST.md new file mode 100644 index 00000000..e31015c8 --- /dev/null +++ b/docs/specs/SPEC-REVIEW-CHECKLIST.md @@ -0,0 +1,68 @@ +# Spec review checklist + +Run a spec past this before accepting it. It exists because JellyTau's +backend/frontend boundary is a **stated rule with, historically, no gate** — the +rule lived in the architecture docs, but nothing forced a spec author to check a +new design against it, and a "minimal-change" spec quietly leaked domain +taxonomy into the frontend (see [scoped-search-boundary.md](scoped-search-boundary.md)). +This checklist is the human gate. The CI check +(`scripts/check-frontend-boundary.sh`) is only a crude tripwire for one leak +signature — it does **not** replace this. + +Copy the boxes into the review comment (or the PR) and tick them. + +## Boundary (the one that bites) + +- [ ] **The spec has a filled-in "Layer assignment" table**, and it assigns + *logic*, not files. A spec without this section is not ready to review. +- [ ] **No domain vocabulary is placed in the frontend.** In particular: Jellyfin + item-type sets that define a *category* (what "Music"/"TV"/"Movies" means), + query-shaping rules, business rules, reachability/sync policy. If the + frontend names a *set* of item types to define a category, that is a leak — + it belongs behind an opaque enum the backend expands. +- [ ] **"The backend already accepts this parameter" was not used as the reason** + to place the deciding logic in the frontend. Accepting a parameter ≠ owning + the decision of its value. +- [ ] **The `Scope:` / effort framing is not optimizing for "least backend + change."** "Frontend only, no Rust changes" is a description, never a goal. + The goal is *correct layer placement*; sometimes that is more Rust work. +- [ ] Ran the litmus test on each borderline responsibility: *would it change if + Jellyfin's API changed?* → Rust. *Only if the UI were redesigned?* → + frontend. Borderline defaults to Rust. +- [ ] Single-type presentation (`itemType: "Movie"`, "this page shows albums") + is **not** over-corrected into the backend. The rule targets category + *taxonomy*, not every mention of a type. Don't invent a backend enum per + list page. + +## IPC contract + +- [ ] Anything crossing the boundary has its wire shape specified. +- [ ] camelCase rule accounted for: top-level params auto-convert; nested structs + get `#[serde(rename_all = "camelCase")]`; tagged unions match tags on both + sides; events are kebab-case. (CLAUDE.md §IPC, + [04-type-sync-and-threading.md](../architecture/04-type-sync-and-threading.md).) +- [ ] Any result that arrives *twice* (command return **and** a later event — + e.g. the search cache/server merge) has **both** payloads in the new shape. +- [ ] `bindings.ts` is regenerated from Rust, not hand-edited. + +## Requirements & traceability + +- [ ] Linked to existing URs, or new URs/DRs are allocated in + [requirements.md](../requirements.md). +- [ ] Requirement-implementing code will carry `// TRACES:` comments (CLAUDE.md). +- [ ] Traceability coverage stays ≥ 50% (the CI gate). + +## Conflicts & hygiene + +- [ ] If this spec revises/supersedes another, the older spec gets a banner + pointing here — no two specs silently contradicting. +- [ ] Acceptance criteria include the standard gates: `bun run check`, + `bun run test`, `bun run check:boundary`, and (if Rust changed) + `cargo fmt`/`cargo clippy`/`bun run test:rust`. +- [ ] Notes flag that a parallel Claude session may be active in the repo. + +--- + +**If any Boundary box can't be ticked, the spec is not ready** — fix the layer +assignment first. Every other section can be negotiated; that one is the whole +reason this file exists. diff --git a/docs/specs/SPEC-TEMPLATE.md b/docs/specs/SPEC-TEMPLATE.md new file mode 100644 index 00000000..e27df44b --- /dev/null +++ b/docs/specs/SPEC-TEMPLATE.md @@ -0,0 +1,105 @@ +# Spec: + + + +**Status:** Proposed +**Requirements:** +**UX spec:** +**Supersedes / revises:** + +## Summary + + + +## Motivation + + + +## Layer assignment + + + +| Logic / responsibility | Layer | Why it belongs there | +|------------------------|-------|----------------------| +| | Rust | | +| | Frontend | | + + + +## Design + + + +## Out of scope + + + +## Acceptance criteria + + + +- [ ] `bun run check` and `bun run test` pass. +- [ ] `cargo fmt` clean, `cargo clippy` clean, `bun run test:rust` passes (if Rust changed). +- [ ] `bun run check:boundary` passes (no taxonomy leak into the frontend). +- [ ] New requirement-implementing code carries `// TRACES:` comments. +- [ ] `bindings.ts` regenerated if Rust types changed. + +## Testing + + + +## TRACES + + + +## Notes for the implementer + + diff --git a/docs/specs/account-menu.md b/docs/specs/account-menu.md new file mode 100644 index 00000000..fd407589 --- /dev/null +++ b/docs/specs/account-menu.md @@ -0,0 +1,164 @@ +# Spec: Account menu and global chrome availability + +**Status:** Implemented +**Scope:** Frontend only. No Rust changes required. +**Requirements:** UR-054 → DR-075, DR-076, DR-077 (see +[requirements.md](../requirements.md)). +**UX spec:** [ux-flows.md §1.2–1.4](../ux-flows.md). + +## Summary + +Account actions — Settings, Downloads, Display preferences, Sign out — are +currently reachable **only from `/library/*`**. Move them into a single shared +account menu anchored to the user's name, and make that menu available on every +authenticated non-immersive screen. + +## Motivation + +A user sitting on the home screen cannot open Settings or sign out. The bottom +nav offers Home / Search / Library only, and the header that hosts those actions +belongs to the library layout. The user has to guess that account actions live +*inside* Library — an unrelated section — and navigate there first. + +Desktop and mobile also disagree today: desktop shows an unlabeled logout icon +with no grouped menu, mobile shows a three-dot overflow with labelled items. The +same two actions are found two different ways. + +## Background: verified current state + +1. **The header is not global.** It is defined in + [library/+layout.svelte](../../src/routes/library/+layout.svelte). The root + layout [+layout.svelte](../../src/routes/+layout.svelte) renders no header at + all. + +2. **`routeOwnsLayout`** in + [layoutShell.ts](../../src/lib/utils/layoutShell.ts) returns true for + `/library`, `/player/`, `/login` — those routes own their own full-height + flex column. Everything else renders into the root scroller with the root's + `BottomUi` below it. + +3. **Bottom nav is Home / Search / Library only** + ([BottomNav.svelte](../../src/lib/components/BottomNav.svelte)) — no Settings + or account entry. + +4. **Net effect:** on `/`, `/search`, and `/downloads` there is no route to + Settings or Sign out. + +5. **Desktop username is inert text** — a `` next to the icons, not a + trigger. + +6. **The mobile overflow menu already has the right contents** (Downloads, + Settings, divider, Sign out) and the right dismissal behaviour (backdrop + click, keyboard handler). **Extract and reuse it rather than rewriting it.** + +7. **`viewMode` is already a persisted store** in + [library.ts](../../src/lib/stores/library.ts) (`jellytau-view-mode`, + `localStorage`). The Display setting is a second view onto it — **no new + state, no migration.** + +## Design + +### `AccountMenu` component (DR-075) + +One component used by both breakpoints. Contents in fixed order: + +``` +Signed in as ← identity block, not interactive + +──────────────────────── +Downloads +Settings +Display ← grid/list preference +──────────────────────── +Sign out ← destructive, last, after a divider +``` + +- **Trigger is the username/avatar**, not a bare three-dot icon. On mobile where + horizontal space is tight, the avatar (or initial) alone is acceptable; the + name shows inside the open menu regardless. +- **Same items, same order, both platforms.** +- Preserve the existing dismissal behaviour: click-outside backdrop, `Escape`, + and focus return to the trigger on close. +- Menu items are real links/buttons — keyboard reachable, correct roles, + `aria-expanded` on the trigger. + +"Display" may either navigate to the Settings Display section or expose the +grid/list choice inline. Prefer navigating — it keeps one source of truth for +preferences and avoids a nested control inside a dropdown. + +### Global chrome (DR-076) + +Make the header — and therefore the account menu — available on `/`, `/search`, +and `/downloads`. + +The cleanest route is to lift the header out of the library layout into a shared +component rendered by the root layout, with the library layout consuming the +same component rather than defining its own. **Do not duplicate the markup into +each route.** + +Constraints that must survive the change: + +- `/player/*` and `/login` stay chrome-free. +- `/settings` already owns its layout; it needs no account menu (the user is + already there), but must not double up on chrome. +- The root layout's flex/scroller structure is deliberate — the comments in + [layoutShell.ts](../../src/lib/utils/layoutShell.ts) and + [+layout.svelte](../../src/routes/+layout.svelte) explain why routes own their + own column. Preserve the scroll containment; a regression here reintroduces + the "last row hidden behind the nav" bug called out in those comments. +- Mini-player and bottom-nav visibility rules (`showGlobalMiniPlayer`, + `showBottomNav`) must be unchanged. + +### Display section in Settings (DR-077) + +Add a Display section to [settings/+page.svelte](../../src/routes/settings/+page.svelte) +with the grid/list control bound to the existing `viewMode` store via +`library.setViewMode(...)`. The page-header toggle in `LibraryGrid` stays — both +controls drive the same store, so they stay in sync for free. + +## Out of scope + +- Redesigning the Settings page or reorganising its existing sections. +- Multi-server / account switching (UR-047) — the identity block displays the + active server but offers no switcher. +- Changing the bottom nav's three destinations. + +## Acceptance criteria + +- [ ] Settings and Sign out are reachable from `/`, `/search`, and `/downloads` + without first navigating into Library. +- [ ] Desktop and mobile show the same account menu items in the same order. +- [ ] The username/avatar opens the menu; it is a real button with + `aria-expanded`. +- [ ] Sign out is last, after a divider, and still logs out + resets library + state + redirects as it does today. +- [ ] `/player/*` and `/login` remain chrome-free. +- [ ] Settings has a Display section that changes grid/list, and the change is + immediately reflected by the library page-header toggle (same store). +- [ ] No regression in scroll containment, mini-player visibility, or bottom-nav + visibility on any route. +- [ ] `bun run check` and `bun run test` pass. + +## Testing + +- Extend the existing `layoutShell` tests: chrome-visibility for `/`, `/search`, + `/downloads` (now true) and `/player/*`, `/login` (still false). +- `AccountMenu`: renders the documented items in order; trigger toggles + `aria-expanded`; `Escape` and backdrop click close it; Sign out invokes the + logout handler. +- Display setting: writes through to the `viewMode` store and persists. + +New requirement-implementing code needs `TRACES:` comments — see +[CLAUDE.md](../../CLAUDE.md). Suggested: `AccountMenu` → `UR-054 | DR-075`, +shell/header changes → `UR-054 | DR-076`, Settings Display section → +`UR-054, UR-029 | DR-077`. + +## Notes for the implementer + +- Read [ux-flows.md §1.2–1.4](../ux-flows.md) first — behavioural spec; this is + the implementation plan. +- The layout shell is subtle and the existing comments record real bugs that + were fixed there. Read them before restructuring. +- Another session may be active in this repo, including in + `src/routes/settings/+page.svelte`. Check `git diff` before "repairing" + unexpected changes, and expect to coordinate on that file. diff --git a/docs/specs/downloads-as-offline-library.md b/docs/specs/downloads-as-offline-library.md new file mode 100644 index 00000000..d22b80ab --- /dev/null +++ b/docs/specs/downloads-as-offline-library.md @@ -0,0 +1,166 @@ +# 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. diff --git a/docs/specs/offline-downloaded-only-filter.md b/docs/specs/offline-downloaded-only-filter.md new file mode 100644 index 00000000..b01fd28a --- /dev/null +++ b/docs/specs/offline-downloaded-only-filter.md @@ -0,0 +1,235 @@ +# Spec: Offline "downloaded only" filtering (issue #10) + +**Status:** Implemented +**Scope:** Frontend (connectivity store) + Rust (hybrid repository). No new +commands, no schema changes, no UI additions. +**Requirements:** UR-052 → DR-078, DR-079, DR-080 +(see [requirements.md](../requirements.md)). +**Tracking:** issue #10 — *"when offline the filter to show only downloaded +media does not work."* + +## Summary + +Offline, a library page is supposed to show **only media on the device**, with a +"Show all server media" toggle that additionally reveals the cached server +catalog greyed out (queueable for download on reconnect). In practice the toggle +does not gate the listing — every server item still appears. This spec fixes +that with two independent changes; either one alone leaves the bug visible. + +## Background: what already exists + +Verified in code. **The feature is built and mostly correct — this is a +two-point repair, not new infrastructure.** Do not rebuild the toggle, the +command, or the SQL gate. + +1. **The SQL gate works and is unit-tested.** + [offline.rs](../../src-tauri/src/repository/offline.rs) — `get_items` appends + the synced-catalog `UNION` branch only when `include_catalog_browse()` is + true; with it false, only downloaded/local rows return. Guarded by + `test_get_items_toggle_gates_synced_catalog` (UT-067). **Do not touch the + query.** + +2. **The toggle → backend path is wired.** The `showServerCatalog` store and the + `set_show_server_catalog` command + ([catalog.rs](../../src-tauri/src/commands/catalog.rs)) drive the process-wide + `INCLUDE_CATALOG_BROWSE` flag. `pushCatalogVisibility` in + [offlineCatalog.ts](../../src/lib/services/offlineCatalog.ts) computes + `include = connected || showCatalog` and pushes it on every change. + +3. **Home-screen queries are already downloads-only.** `get_latest_items`, + `get_resume_items`, `get_recently_played_audio`, `get_resume_movies` all + `INNER JOIN downloads ... status = 'completed'`. They are unaffected — leave + them. + +4. **`MediaCard` already greys and queues.** + [MediaCard.svelte](../../src/lib/components/library/MediaCard.svelte) — + `isServerOnly` renders the greyed, inert card with a queue button; the queued + row heals its `stream_url` on reconnect via the offlineCatalog service. Leave + it. + +## The two defects + +### Defect A — offline is never actually entered (DR-079) + +`pushCatalogVisibility` keys off `isConnected`, but +[connectivity.ts](../../src/lib/stores/connectivity.ts) derives: + +```ts +isConnected = isOnline && isServerReachable // isOnline = navigator.onLine +``` + +`navigator.onLine` is documented in that same file as **advisory only** — the +Rust `ConnectivityMonitor` is the source of truth (principle: *reachability from +real traffic*, DR-055). When the server is unreachable but the device link is +up (server down, wrong LAN, VPN dropped), `isOnline` stays true, so `isConnected` +stays true, so `include` stays true, so the backend keeps returning the full +catalog. The user is "offline" in every meaningful sense but the toggle never +gets a chance to gate anything. + +This is the primary cause: it explains why the filter looks dead rather than +merely inverted — the gate never closes. + +### Defect B — an intentionally empty result falls through to the server (DR-080) + +With the gate off and nothing downloaded in a library, offline `get_items` +correctly returns few or zero rows. But +[hybrid.rs](../../src-tauri/src/repository/hybrid.rs) treats a cache result as a +hit only `if data.has_content()`. An empty offline result is indistinguishable +from a cache miss, so `HybridRepository::get_items` (and `parallel_race`, used by +~10 other reads) falls through to the server and returns the full server list — +re-defeating the filter even after Defect A is fixed. + +## Design + +### Fix A: `isConnected` follows backend reachability alone (DR-079) + +In [connectivity.ts](../../src/lib/stores/connectivity.ts), redefine the derived +store: + +```ts +export const isConnected = derived( + connectivity, + ($c) => $c.isServerReachable +); +``` + +`navigator.onLine` stays wired to what it is good for — a *trigger* for an +immediate recheck (`online`/`offline` listeners already call +`checkServerReachable()`); it must no longer be a *term* in the offline decision. +Leave `isOnline` on the state object and the listeners intact. + +Consider whether the optimistic `isServerReachable: true` startup default +([connectivity.ts](../../src/lib/stores/connectivity.ts)) should hold until the +first real check resolves. Keep it — flipping the app to "offline" on launch is a +worse regression than a brief full-catalog flash before the first probe. Note the +choice in a comment. + +**Blast radius — this is the reason this is a spec, not a patch.** `isConnected` +is consumed beyond this feature (banners, `MediaCard`, mini-player gating, +anything importing it). Enumerate consumers first: + +``` +grep -rn "isConnected" src/ | grep -v node_modules +``` + +For each, confirm "server unreachable" (not "device link down") is the correct +trigger. It almost always is — that is the whole point of the reachability model +— but verify rather than assume, and call out anything that genuinely wanted the +device link in the PR description. + +### Fix B: an empty offline result is authoritative when the gate is off (DR-080) + +The backend must distinguish "cache is cold, go ask the server" from "user asked +for downloads only and there are none here." The gate flag already encodes intent +— reuse it. + +Add a getter beside the existing setter in +[offline.rs](../../src-tauri/src/repository/offline.rs): + +```rust +pub fn include_catalog_browse() -> bool { /* pub, already exists privately */ } +``` + +In [hybrid.rs](../../src-tauri/src/repository/hybrid.rs) `get_items`: when +`!include_catalog_browse()`, treat the offline result as authoritative and return +it **as-is even when empty** — do not spawn/await the server fallback for this +call. When the flag is on (online fast-path, or offline with the toggle on), +behaviour is unchanged: empty cache still falls through to the server. + +Keep it surgical: + +- Scope the change to `get_items`. The gate is a `get_items` concept; do not + thread it into `parallel_race` or the other readers, which have no catalog + gate and legitimately want the server on an empty cache. +- Preserve the online path exactly: with the flag on (its default, and always so + while reachable) the method behaves as it does today, including the background + cache refresh on a hit. +- The flag is process-global `Relaxed`; it is set from the frontend before the + query. That ordering already holds for the SQL gate — no new synchronization. + +### Why both + +Fix A closes the gate; Fix B stops the hybrid from re-opening it. A alone: with +downloads present the list still gets padded by the server fallback whenever a +library's cache is thin. B alone: the gate never closes because `isConnected` +never goes false on a live link. Ship them together. + +## Out of scope + +- The SQL gate, the toggle, the command, `INCLUDE_CATALOG_BROWSE` — all correct. +- `MediaCard` greying / queue-on-reconnect — correct. +- Home-screen and resume queries — already downloads-only. +- The Rust `ConnectivityMonitor` reachability logic itself — unchanged; this + spec only stops the *frontend* from diluting its verdict with `navigator.onLine`. +- Any new IPC command, DB column, or settings entry. +- Making the "Show all server media" toggle reachable from Settings (that is a + UX-placement question, tracked separately under UR-051's toggle note). + +## Acceptance criteria + +- [~] With the server unreachable on a live device link, a library page lists + only downloaded media when the toggle is off (IT-016 — pending e2e; unit + coverage via UT-069 + gate tests). +- [x] Turning the toggle on reveals the greyed-out cached catalog; turning it off + hides it again — without leaving/re-entering the page (SQL gate + toggle + wiring unchanged; UT-068 confirms the flag is pushed on toggle change). +- [x] A library with downloads and a thin cache does not get padded with + non-downloaded server items when offline with the toggle off (Defect B — + UT-070: gate off + empty offline result returned as-is, server not queried). +- [x] `isConnected` is false whenever the server is unreachable, regardless of + `navigator.onLine`; true for a reachable server even if the browser reports + offline (UT-069). +- [x] Every existing `isConnected` consumer still behaves correctly (banner in + `+layout.svelte`, `MediaCard`, `favorites.ts` server-write skip — all want + "server unreachable", which is the new semantics; `CastButton`'s local + `isConnected` is unrelated). Full frontend suite (616 tests) green. +- [x] Online behaviour is unchanged: with the flag on (its default, always so + while reachable) `get_items` keeps the offline fast-path and background + refresh (UT-067 + gate-on fall-through test). +- [~] A download queued from a greyed offline card resolves and starts on + reconnect (IT-017 — regression check, no code change; offlineCatalog + resume path untouched). +- [x] `bun run check`, `bun run test`, and `bun run test:rust` pass; + `cd src-tauri && cargo fmt && cargo clippy` clean (no new warnings in the + touched files). + +## Testing + +Rust ([offline.rs](../../src-tauri/src/repository/offline.rs) / +[hybrid.rs](../../src-tauri/src/repository/hybrid.rs) test modules): + +- **UT-070** — hybrid `get_items` with the gate off returns an empty offline + result as-is and does **not** query the server. Assert via a mock online repo + whose `get_items` bumps a call counter that must stay at zero. +- Gate on + empty cache still falls through to the server (guard the online path). +- UT-067 (`test_get_items_toggle_gates_synced_catalog`) must still pass untouched. + +Frontend (vitest, `src/lib/**/*.test.ts`): + +- **UT-069** — `isConnected` follows `isServerReachable` alone: false when + unreachable with `navigator.onLine === true`; true when reachable with + `navigator.onLine === false`. +- **UT-068** — `pushCatalogVisibility` resolves `serverReachable || showCatalog` + and pushes to the backend on a change of either input (extend the existing + offlineCatalog tests). + +Integration (IT-016, IT-017) are documented as pending in +[requirements.md](../requirements.md); wire them if the e2e harness can simulate +an unreachable-server-on-live-link state, otherwise leave them pending with a note. + +New/changed requirement code keeps its `TRACES:` comments — see +[CLAUDE.md](../../CLAUDE.md). The affected files already carry tags: +`connectivity.ts` (`… | DR-079`), `hybrid.rs` (`… | DR-080`), `offline.rs` +(`… | DR-078`). Update the getter's tag when you expose it. + +## Notes for the implementer + +- Read [docs/architecture/07-connectivity.md](../architecture/07-connectivity.md) + before Fix A — it is the canonical statement of the reachability model this fix + restores fidelity to. +- Fix B relies on the frontend having pushed the flag before the query runs; that + ordering already holds for the SQL gate today. No new locking. +- Another session is active in this repo (WiFi-only downloads, account menu + landed alongside this work). Check `git diff` before "repairing" unexpected + changes, and expect requirement IDs around UR-052 / DR-078 to be adjacent to + other new rows. diff --git a/docs/specs/scoped-search-boundary.md b/docs/specs/scoped-search-boundary.md new file mode 100644 index 00000000..c0a6b666 --- /dev/null +++ b/docs/specs/scoped-search-boundary.md @@ -0,0 +1,273 @@ +# Spec: Move search scope taxonomy behind the Rust boundary + +**Status:** Proposed +**Scope:** Rust + Frontend. **Revises a decision in +[scoped-search.md](scoped-search.md).** +**Requirements:** UR-049, UR-050 (existing) → new DRs for the boundary move +(allocate on implementation; suggested DR-063/DR-065/DR-067 revisions plus one +new DR for the grouped result shape — see [requirements.md](../requirements.md)). +**UX spec:** unchanged — [ux-flows.md §6](../ux-flows.md). This is a pure +architecture/boundary change with **no user-visible behaviour difference**. + +## Why this spec exists + +[scoped-search.md](scoped-search.md) shipped scoped search as "frontend only, no +Rust changes." That was the smallest wiring change, and it worked — but it left +**Jellyfin's item-type taxonomy encoded in the presentation layer**, which +violates the project's core boundary rule ("Svelte frontend — presentation +only"; all business logic in Rust — see [CLAUDE.md](../../CLAUDE.md) and +[architecture/02-svelte-frontend.md](../architecture/02-svelte-frontend.md)). + +The offending knowledge lives in +[searchScope.ts](../../src/lib/utils/searchScope.ts): + +```ts +const SCOPE_ITEM_TYPES = { + music: ["MusicAlbum", "MusicArtist", "Audio", "Playlist"], + movies: ["Movie"], + tv: ["Series", "Episode"], +}; +const GROUP_ITEM_TYPES = { + songs: ["Audio"], albums: ["MusicAlbum"], artists: ["MusicArtist"], + movies: ["Movie"], tvShows: ["Series", "Episode"], +}; +``` + +This is a **domain definition** — "what the category *Music* means in Jellyfin's +vocabulary" — expressed twice, in the wrong layer. The concrete failure it +creates: the day the backend starts returning a type the frontend never +enumerated (e.g. `MusicVideo`, or Jellyfin renaming a kind), search silently +drops it from both the query filter and the result buckets, and nothing in the +Rust layer — the actual authority on Jellyfin's API — can correct it. Two +sources of truth that will drift. + +**This must be fixed while the feature is uncommitted**, before the leak ships +baked into a released wire contract. + +### What is *not* a leak (leave it alone) + +Single concrete-type list pages are **not** business logic and stay as-is: + +- `music.ts` → `["MusicAlbum"]` / `["Playlist"]`, `movies.ts` → `["Movie"]`, + `tv.ts` → `["Series"]` +- `GenericMediaListPage.svelte` → `[config.itemType]` +- `ArtistDetailView`, `RelatedItemsSection`, `AddToPlaylistModal`, + `PersonDetailView` + +"This page shows albums" is a legitimate presentation choice expressed through a +generic `getItems(parentId, { includeItemTypes })` API. Only the **search scope +taxonomy** (a semantic category → many types, defined once and reused) crosses +the line. Do **not** invent a backend enum for every list page — that is +over-abstraction, not cleaner separation. + +## The boundary rule after this change + +> The frontend never names a Jellyfin item type **in connection with search.** +> It sends an opaque `scope`, and receives results already sorted into labelled +> groups. The frontend owns only **group order** (presentation) and +> **rendering**. + +## Design + +### Rust owns scope → item-types (query side) + +Add an opaque enum that crosses IPC, and move the expansion table into Rust: + +```rust +// repository/types.rs +#[derive(specta::Type, Serialize, Deserialize, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub enum SearchScope { All, Music, Movies, Tv } + +impl SearchScope { + /// The Jellyfin item types this scope requests, or None for `All` + /// (which must send NO includeItemTypes — see below). + pub fn item_types(self) -> Option> { + match self { + SearchScope::All => None, + SearchScope::Music => Some(vec!["MusicAlbum", "MusicArtist", "Audio", "Playlist"] + .into_iter().map(String::from).collect()), + SearchScope::Movies => Some(vec!["Movie".into()]), + SearchScope::Tv => Some(vec!["Series".into(), "Episode".into()]), + } + } +} +``` + +`SearchOptions` gains `scope` and the search command resolves it into the +existing `include_item_types` filter **inside Rust**, before dispatching to the +online/offline paths (which already honour `include_item_types` — do not touch +their filtering, per [scoped-search.md](scoped-search.md) §Background 2). + +```rust +pub struct SearchOptions { + pub limit: Option, + pub search_term: Option, + pub scope: Option, // NEW + // include_item_types stays for the single-type list-page callers, + // but the SEARCH command derives it from `scope` when scope is set. +} +``` + +**Precedence:** if `scope` is set it wins; `include_item_types` remains for the +non-search `getItems` callers. Document this so a future reader does not send +both. + +**`All` sends no filter.** Preserve the existing invariant: `All` must omit +`includeItemTypes` entirely, not send the union of every enumerated type — types +nobody listed (Person, folders) would otherwise be filtered out. This is why +`item_types()` returns `Option`, and the command must skip the filter on `None`. + +### Rust owns result bucketing (result side) + +Results arrive **pre-grouped**. Rust classifies each returned `MediaItem` into a +group by its type — the `GROUP_ITEM_TYPES` knowledge, moved to the authority: + +```rust +#[derive(specta::Type, Serialize, Deserialize, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub enum SearchGroupId { Songs, Albums, Artists, Movies, TvShows } + +#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SearchGroup { pub id: SearchGroupId, pub items: Vec } + +#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GroupedSearchResult { pub groups: Vec } +``` + +Rust emits **every** non-empty group it can classify, in a stable canonical +order. It does **not** apply the user's ordering or drop out-of-scope groups — +those are presentation and stay frontend-side (see below). Items whose type maps +to no group are omitted from grouped output (same as today's frontend filter). + +### 🔴 The `search-event` wrinkle — both payloads must change + +Search returns results **twice**: the command resolves with instant local-cache +results, then the merged cache+server union arrives later via the `search-event` +listener (see [library.ts](../../src/lib/stores/library.ts) `search()` and +[architecture/03-data-flow.md](../architecture/03-data-flow.md)). **Both** the +command return value **and** the `search-event` payload must carry +`GroupedSearchResult`. If only one is converted, the instant results group and +the merged ones do not (or vice versa), and the UI flickers between shapes. This +is the single largest part of the change and the easiest to half-do. + +### What the frontend keeps (all pure presentation) + +[searchScope.ts](../../src/lib/utils/searchScope.ts) **retains**: + +- `SearchScope` type — now sourced from the generated bindings, mirroring the + Rust enum (delete the hand-written union). +- `SCOPE_LABELS`, `SEARCH_SCOPES` (chip labels / order). +- `resolveSearchScope(pathname)` — route → initial scope. Pure, DOM-free, + unit-tested. **Stays exactly as-is.** +- `SearchGroupId` (from bindings), `GROUP_LABELS`. +- `normalizeGroupOrder`, `groupsForScope`, `moveGroup`, `reorderGroups`, + `DEFAULT_GROUP_ORDER` — group-order persistence and reordering, all + presentation. + +[searchScope.ts](../../src/lib/utils/searchScope.ts) **loses**: + +- `SCOPE_ITEM_TYPES`, `GROUP_ITEM_TYPES` (moved to Rust). +- `scopeItemTypes()`, `groupItemTypes()`. +- The `.type`-inspecting body of `composeSearchGroups()`. + +`composeSearchGroups()` shrinks to a **presentation composition over Rust's +groups** — no `.type` inspection anywhere: + +```ts +// Take Rust's pre-bucketed groups; drop out-of-scope, sort by saved order, +// attach labels, omit empties. No Jellyfin type vocabulary. +composeSearchGroups(groups: SearchGroup[], scope, order): DisplayGroup[] +``` + +`GROUP_SCOPE` (which group belongs to which scope) is a borderline case: it is +"is Songs part of the Music scope," arguably taxonomy. But because Rust already +filtered the query by scope, out-of-scope groups will simply be **empty** and +drop out via the empty-omit rule — so the frontend does not strictly need +`GROUP_SCOPE` for correctness once Rust filters. **Recommendation:** delete +`GROUP_SCOPE` and rely on empty-omission; if kept for belt-and-suspenders, treat +it as a display hint, not authority. + +### Frontend call-site changes + +- [library.ts](../../src/lib/stores/library.ts) `search(query, scope)` sends + `{ scope }` in `SearchOptions` instead of computing `includeItemTypes`. + Everything else (requestId bump, stale guard, 10s timeout, empty-query clear, + event merge) is preserved. +- [SearchResults.svelte](../../src/lib/components/search/SearchResults.svelte) + consumes `SearchGroup[]` from the store instead of a flat `MediaItem[]` + + client-side `composeSearchGroups(results, …)`. The store now holds grouped + results. +- [search/+page.svelte](../../src/routes/search/+page.svelte) is unchanged in + behaviour; only the type it passes to `SearchResults` changes. + +## Out of scope + +- Any change to online/offline `include_item_types` **filtering** — it already + works; only the *source* of the type list moves. +- Single concrete-type list pages (see "What is not a leak"). +- Ranking within or across groups. +- The UX / chip behaviour / persistence mechanism — all unchanged from + [scoped-search.md](scoped-search.md). + +## Acceptance criteria + +- [ ] No Jellyfin item-type string literal (`"MusicAlbum"`, `"Audio"`, …) remains + in `searchScope.ts` or any search call path. Verify: + `grep -rn '"MusicAlbum"\|"MusicArtist"\|"Audio"\|"Series"\|"Episode"\|"Movie"\|"Playlist"' src/lib/utils/searchScope.ts src/lib/stores/library.ts` returns nothing. +- [ ] `SearchScope` and `SearchGroupId` in the frontend come from the generated + `bindings.ts`, not hand-written unions. +- [ ] Search behaviour is **identical** to today for the user: same scoping, same + groups, same order, same empty/out-of-scope omission, offline included. +- [ ] Both the command return and the `search-event` payload carry the grouped + shape; no shape flicker between instant and merged results. +- [ ] `All` scope still sends no `includeItemTypes` (assert in a Rust test). +- [ ] Adding a hypothetical new type to a scope requires editing **only** Rust. +- [ ] `cargo fmt` clean, `cargo clippy` clean, `bun run test:rust` passes. +- [ ] `bun run check` and `bun run test` pass; `bindings.ts` regenerated and + committed. + +## Testing + +**Rust** (`src-tauri`, `cargo test`): +- `SearchScope::item_types()`: each scope's list, and `All` → `None`. +- Search command: `scope: Music` resolves to the four music types on the query; + `scope: All` sends no `include_item_types`. +- Bucketing: a mixed `Vec` classifies into the right `SearchGroupId`s; + unknown types are dropped; groups come out in canonical order. +- The `search-event` payload is the grouped shape (guard the wrinkle). + +**Frontend** (vitest, `src/lib/**/*.test.ts`) — update existing tests: +- `librarySearchScope.test.ts` currently asserts `includeItemTypes` on the + outgoing options — **rewrite** to assert `scope` is sent instead. +- `searchScope.test.ts` — drop `scopeItemTypes`/`groupItemTypes` cases; keep and + extend `resolveSearchScope`, order normalize/move/reorder, and the new + compose-over-groups (order + empty-omit, no type inspection). +- `searchGroupOrder.test.ts` — unchanged. + +## TRACES + +Per [CLAUDE.md](../../CLAUDE.md), tag requirement-implementing code: +- `SearchScope` enum + `item_types()` + search command scope resolution: + `UR-049 | DR-063` (revised — resolution now Rust-side). +- Grouped result shape + bucketing: `UR-050 | DR-067` (revised) + a new DR for + the wire shape. +- `library.ts` store change: `UR-049 | DR-065` (revised — sends scope not types). + +## Notes for the implementer + +- This spec **revises** [scoped-search.md](scoped-search.md) §Background 2 and + §Design "Scope model / Threading scope through the store," which asserted no + Rust change. Update that spec's status to note the boundary was moved, or add a + banner pointing here — do not leave the two specs contradicting silently. +- The IPC camelCase rule applies to the new enums and structs + ([CLAUDE.md](../../CLAUDE.md)): `#[serde(rename_all = "camelCase")]` on structs; + the tagged-enum tag convention if any enum becomes tagged. Add/extend a + `tauriIntegration`-style test if a new command is introduced. +- Regenerate `bindings.ts` via the tauri-specta build step after changing Rust + types; do not hand-edit it. +- **Another Claude session may be active in these same files** (per project + memory). `git diff` before repairing anything unexpected; these search files + are exactly the ones a parallel session touched. diff --git a/docs/specs/scoped-search.md b/docs/specs/scoped-search.md new file mode 100644 index 00000000..eaae19c3 --- /dev/null +++ b/docs/specs/scoped-search.md @@ -0,0 +1,202 @@ +# Spec: Context-scoped search with filter chips and configurable group order + +> ⚠️ **Superseded in part by +> [scoped-search-boundary.md](scoped-search-boundary.md).** The "frontend only, +> no Rust changes" decision below (§Background 2, §Design "Scope model" and +> "Threading scope through the store") left Jellyfin's item-type taxonomy in the +> presentation layer, which violates the backend/frontend boundary. The taxonomy +> is being moved into Rust. The **user-facing behaviour and UX in this spec are +> unchanged**; only where the scope→item-type mapping and result bucketing live +> changes. Read the boundary spec before touching search code. + +**Status:** Implemented (boundary revision pending — see banner above) +**Scope:** Frontend only. No Rust changes required. *(Revised — see banner.)* +**Requirements:** UR-049 → DR-063, DR-064, DR-065; UR-050 → DR-066, DR-067 +(see [requirements.md](../requirements.md)). +**UX spec:** [ux-flows.md §6](../ux-flows.md) — §6.1 scope, §6.2 layout, +§6.3 group order, §6.4 current deviations. + +## Summary + +Two related changes to search: + +1. **Scope** — a search started inside a library searches *that* library. + Started from Home, `/library`, or the search tab, it searches everything. + The active scope shows as a chip row under the search bar, preselected from + context and freely changeable without retyping. +2. **Group order** — the order result groups appear in (Songs, Albums, Artists, + Movies, TV Shows) becomes a drag-and-drop setting instead of being hardcoded. + +## Motivation + +Searching "office" while browsing TV currently returns music albums, because +both search entry points call the same unscoped query. The user has already +told us what they're looking at; ignoring that makes search feel indiscriminate +and pushes the relevant result below unrelated media. + +## Background: what already exists + +Verified in code — **most of the plumbing is already there.** This is +substantially a wiring task, not new infrastructure. + +1. **`SearchOptions` already carries the filter.** + [bindings.ts](../../src/lib/api/bindings.ts) — + `SearchOptions = { limit?, includeItemTypes?, searchTerm? }`. + +2. **Rust already honours `include_item_types` on both paths** — online + ([online.rs](../../src-tauri/src/repository/online.rs), in the `get_items` + options mapping) and offline + ([offline.rs](../../src-tauri/src/repository/offline.rs), which builds a SQL + type filter from it). **Do not add Rust code for filtering.** + +3. **Per-page list search already does this correctly.** + [GenericMediaListPage.svelte](../../src/lib/components/library/GenericMediaListPage.svelte) + passes `includeItemTypes: [config.itemType]` to `repo.search(...)`. Use it as + the reference for the call shape, including the `requestId` handling. + +4. **The gap is exactly one function.** + [library.ts](../../src/lib/stores/library.ts) — `search(query)` takes only a + query and calls `repo.search(query, { limit: 10000 }, requestId)`, dropping + any scope. Both callers + ([search/+page.svelte](../../src/routes/search/+page.svelte) and + [library/+layout.svelte](../../src/routes/library/+layout.svelte)) go through + it. + +5. **Group order is hardcoded in markup.** + [SearchResults.svelte](../../src/lib/components/search/SearchResults.svelte) + categorizes into `music{tracks,albums,artists} / movies / tvShows` and + renders three fixed sections in source order. + +6. **Frontend preferences persist via `localStorage`**, per the existing + `viewMode` precedent in [library.ts](../../src/lib/stores/library.ts) + (`jellytau-view-mode`). Follow that pattern — **do not** add a Rust settings + command for this. + +## Design + +### Scope model + +One `SearchScope` type, defined once and shared: + +| Scope | `includeItemTypes` | Chip label | +|-------|--------------------|------------| +| `all` | *unset* | All | +| `music` | `MusicAlbum`, `MusicArtist`, `Audio`, `Playlist` | Music | +| `movies` | `Movie` | Movies | +| `tv` | `Series`, `Episode` | TV | + +`all` must send **no** `includeItemTypes` key rather than a list of every type — +the two are not equivalent for item types not enumerated here (Person, folders). + +### Route → scope resolution (DR-063) + +A pure function, unit-testable without a DOM: + +```ts +resolveSearchScope(pathname: string): SearchScope +``` + +- `/library/music*` → `music` +- `/library/movies*` → `movies` +- `/library/tv*` → `tv` +- `/`, `/library`, `/search`, anything else → `all` + +Note `/library/shows/genres` exists as a route; treat `shows` as `tv`. Check the +current route list before finalising — do not assume this table is exhaustive. + +### Scope is a starting point, not a lock (DR-064) + +The resolved scope sets the **initial** chip only. Once the user taps a chip, +their choice governs until they leave the search surface. Concretely: derive the +initial value from the route, hold it in component state, and do not re-derive +it on every navigation — otherwise a user who widens to All snaps back to TV. + +Changing a chip re-runs the current query at the new scope. Changing the query +keeps the current scope. + +### Threading scope through the store (DR-065) + +Extend the store's search signature to accept an optional scope and pass +`includeItemTypes` down to `repo.search`. Preserve the existing behaviour +exactly: the `requestId` bump, the stale-response guard, the `search-event` +listener merge, the 10s timeout, and the empty-query clear path. This is an +additive parameter — no caller should break. + +### Group order (DR-066, DR-067) + +Persist an ordered array of group ids: + +``` +["songs", "albums", "artists", "movies", "tvShows"] // shipped default +``` + +Rendering composes scope and order as **two independent axes**, in this order: + +1. drop groups outside the active scope, +2. sort the remainder by the user's saved order, +3. omit groups that came back empty. + +Scope never rewrites the saved order — narrowing to Music and back to All must +restore the user's full arrangement. See [ux-flows.md §6.3](../ux-flows.md) for +the worked example. + +Settings gets a reorderable list. **Dragging alone is not sufficient**: provide +keyboard-operable move up/down controls with proper labels, or the setting is +unusable with a screen reader and on any pointerless input. + +Unknown or missing ids in the stored array must not crash rendering — treat the +stored order as a hint, append any group it doesn't mention, and ignore ids that +no longer exist. A user upgrading from a build with fewer groups must not lose +the new ones. + +## Out of scope + +- Ranking *within* a group. Order is presentation-only. +- Server-side search ranking or the Jellyfin query itself. +- Scope chips on the per-page list search in `GenericMediaListPage` — that page + is already implicitly scoped by its own `itemType`. +- Any Rust change. + +## Acceptance criteria + +- [ ] Searching from inside Music returns no movies or TV; from inside TV, no music. +- [ ] Searching from Home, `/library`, or the search tab returns all types. +- [ ] The chip row renders under the search bar on both the search page and the + in-library header search, with the context-derived chip preselected. +- [ ] Tapping a chip re-runs the search with the query preserved; editing the + query preserves the selected chip. +- [ ] Tapping "All" from a context-scoped search widens results without retyping. +- [ ] Result groups render in the user's configured order, with out-of-scope and + empty groups omitted and relative order preserved. +- [ ] Group order is reorderable by drag **and** by keyboard, persists across + restarts, and ships with the documented default. +- [ ] Offline search respects scope (the offline path already filters — verify, + don't reimplement). +- [ ] `bun run check` and `bun run test` pass. + +## Testing + +Follow the existing frontend test conventions (vitest, `src/lib/**/*.test.ts`). + +- `resolveSearchScope` — pure unit tests over the route table, including the + `/library/shows/genres` case and unknown routes falling back to `all`. +- Scope → `includeItemTypes` mapping, asserting `all` omits the key entirely. +- The compose step: scope filter + user order + empty-group omission, including + the "narrow then widen restores order" case and a stored order containing an + unknown id. +- Store-level: scoped search forwards `includeItemTypes` to the repository, and + the existing stale-`requestId` guard still discards superseded responses. + +New requirement-implementing code needs `TRACES:` comments — see +[CLAUDE.md](../../CLAUDE.md). Suggested tags: the scope resolver and chip row +`UR-049 | DR-063, DR-064`, the store change `UR-049 | DR-065`, the settings list +and ordered rendering `UR-050 | DR-066, DR-067`. + +## Notes for the implementer + +- Read [ux-flows.md §6](../ux-flows.md) first — it is the behavioural spec; this + document is the implementation plan. +- The IPC camelCase rule applies to anything new that crosses the boundary + ([CLAUDE.md](../../CLAUDE.md)) — though this change should not add commands. +- Another session may be active in this repo. Check `git diff` before + "repairing" unexpected changes. diff --git a/docs/traceability-ci.md b/docs/traceability-ci.md index c3501629..a6f07b22 100644 --- a/docs/traceability-ci.md +++ b/docs/traceability-ci.md @@ -12,22 +12,22 @@ The CI/CD pipeline automatically validates that code changes are properly traced ## Gitea Actions Workflows -Two workflows are configured in `.gitea/workflows/`: +Traceability validation lives in `.gitea/workflows/traceability-check.yml`: -### 1. `traceability-check.yml` (Primary - Recommended) -Gitea-native workflow with: - ✅ Automatic trace extraction - ✅ Coverage validation against minimum threshold (50%) - ✅ Modified file checking - ✅ Artifact preservation - ✅ Summary reports -**Runs on:** Every push and pull request +**Runs on:** Every push and pull request to `master`/`main`/`develop` -### 2. `traceability.yml` (Alternative) -GitHub-compatible workflow with additional features: -- Pull request comments with coverage stats -- GitHub-specific integrations +A second workflow, `traceability.yml`, previously duplicated this one as a +"GitHub-compatible alternative". It was removed: CI here is Gitea Actions, and +its only unique step (PR comments via `actions/github-script`) depended on the +GitHub REST client, which Gitea does not provide. To add PR comments, post to +Gitea's `/api/v1/repos/{owner}/{repo}/issues/{index}/comments` from +`traceability-check.yml` rather than reviving the old file. ## What Gets Validated diff --git a/docs/traceability.md b/docs/traceability.md index 89042d1c..5cb0bcd4 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,22 +1,22 @@ # Code Traceability Matrix -**Generated:** 7/22/2026, 9:49:16 PM +**Generated:** 7/23/2026, 7:48:39 PM ## Summary -- **Total Files Scanned:** 264 -- **Total TRACES Found:** 182 +- **Total Files Scanned:** 284 +- **Total TRACES Found:** 261 - **Requirements Covered:** - - User Requirements (UR): 42 + - User Requirements (UR): 52 - Integration Requirements (IR): 14 - - Development Requirements (DR): 53 + - Development Requirements (DR): 75 - 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-039, UR-040, UR-041, UR-042, UR-043, UR-044, UR-045, UR-046, UR-047 +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 ``` ### 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-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-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 ``` ### Jellyfin API Requirements (JA) @@ -110,7 +110,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### IR-013 -**Locations:** 2 file(s) +**Locations:** 3 file(s) - **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - **Line:** 3 @@ -118,6 +118,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/storage/schema.rs`](src-tauri/src/storage/schema.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` ### IR-014 @@ -194,11 +197,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 17 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1974) - - **Line:** 1974 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2116) + - **Line:** 2116 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` - **File:** [`src/lib/stores/player.ts`](src/lib/stores/player.ts#L8) - **Line:** 8 @@ -230,18 +233,18 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/index.ts`](src/lib/player/index.ts#L15) - **Line:** 15 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L25) - - **Line:** 25 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) - **Line:** 6 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) - **Line:** 23 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L25) + - **Line:** 25 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` @@ -250,8 +253,8 @@ 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/api/bindings.ts`](src/lib/api/bindings.ts#L1860) - - **Line:** 1860 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2002) + - **Line:** 2002 - **Context:** `export type PlaybackProgress = { itemId: string; positionTicks: number...` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L42) - **Line:** 42 @@ -261,8 +264,8 @@ 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/api/bindings.ts`](src/lib/api/bindings.ts#L1956) - - **Line:** 1956 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2098) + - **Line:** 2098 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L117) - **Line:** 117 @@ -292,8 +295,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 13 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2179) - - **Line:** 2179 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2321) + - **Line:** 2321 - **Context:** `export type RemoteSessionStatus = { position: number; duration: number...` - **File:** [`src/lib/stores/queue.test.ts`](src/lib/stores/queue.test.ts#L4) - **Line:** 4 @@ -362,9 +365,6 @@ 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/GenericMediaListPage.svelte`](src/lib/components/library/GenericMediaListPage.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src/lib/stores/movies.ts`](src/lib/stores/movies.ts#L3) - **Line:** 3 - **Context:** `Unknown` @@ -374,6 +374,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/music.ts`](src/lib/stores/music.ts#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` ### DR-009 @@ -400,12 +403,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/appState.ts`](src/lib/stores/appState.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src/lib/utils/layoutShell.test.ts`](src/lib/utils/layoutShell.test.ts#L14) - - **Line:** 14 - - **Context:** `Unknown` - **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L17) - **Line:** 17 - **Context:** `Unknown` +- **File:** [`src/lib/utils/layoutShell.test.ts`](src/lib/utils/layoutShell.test.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/player/index.ts`](src/lib/player/index.ts#L15) - **Line:** 15 - **Context:** `Unknown` @@ -458,7 +461,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-012 -**Locations:** 5 file(s) +**Locations:** 7 file(s) - **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - **Line:** 3 @@ -466,19 +469,25 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/schema.rs`](src-tauri/src/storage/schema.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` - **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 {` -- **File:** [`src-tauri/src/storage/schema.rs`](src-tauri/src/storage/schema.rs#L3) +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) - **Line:** 3 - **Context:** `Unknown` ### DR-013 -**Locations:** 3 file(s) +**Locations:** 5 file(s) - **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) - **Line:** 10 @@ -489,6 +498,12 @@ 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/online.rs`](src-tauri/src/repository/online.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### DR-014 @@ -526,8 +541,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/services/preload.ts`](src/lib/services/preload.ts#L5) - **Line:** 5 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 - **Context:** `Unknown` ### DR-016 @@ -562,8 +577,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/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 - **Context:** `Unknown` ### DR-020 @@ -679,8 +694,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 18 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` - **File:** [`src/lib/utils/duration.test.ts`](src/lib/utils/duration.test.ts#L4) - **Line:** 4 @@ -738,8 +753,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 9 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2221) - - **Line:** 2221 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2363) + - **Line:** 2363 - **Context:** `export type SessionInfo = { id?: string | null; userId?: string | null...` - **File:** [`src/lib/components/player/SleepTimerModal.svelte`](src/lib/components/player/SleepTimerModal.svelte#L1) - **Line:** 1 @@ -753,15 +768,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L301) - **Line:** 301 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/sleep_timer.rs`](src-tauri/src/player/sleep_timer.rs#L4) - **Line:** 4 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/sleep_timer.rs`](src-tauri/src/player/sleep_timer.rs#L81) - **Line:** 81 - **Context:** `pub fn cancel(&mut self) {` +- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -945,6 +960,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 3 - **Context:** `Unknown` +### DR-043 + +**Locations:** 1 file(s) + +- **File:** [`src/routes/library/[id]/+page.svelte`](src/routes/library/[id]/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + ### DR-044 **Locations:** 1 file(s) @@ -965,8 +988,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 13 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` - **File:** [`src/lib/stores/nextEpisode.ts`](src/lib/stores/nextEpisode.ts#L9) - **Line:** 9 @@ -992,15 +1015,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L340) - **Line:** 340 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) - **Line:** 6 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) - **Line:** 23 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -1057,7 +1080,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-052 -**Locations:** 8 file(s) +**Locations:** 9 file(s) - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) - **Line:** 38 @@ -1065,23 +1088,26 @@ 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#L51) - **Line:** 51 - **Context:** `Unknown` -- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` - **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` - **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#L543) - - **Line:** 543 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L68) + - **Line:** 68 + - **Context:** `pub struct VideoSettingsWrapper(pub Mutex);` +- **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#L623) - - **Line:** 623 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L658) + - **Line:** 658 - **Context:** `Unknown` ### DR-053 @@ -1102,8 +1128,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-055 -**Locations:** 1 file(s) +**Locations:** 2 file(s) +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/connectivity.rs`](src-tauri/src/commands/connectivity.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -1148,6 +1177,408 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 3 - **Context:** `Unknown` +### DR-061 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](src/lib/components/library/EpisodeFocusView.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### DR-062 + +**Locations:** 2 file(s) + +- **File:** [`src/routes/library/[id]/+page.svelte`](src/routes/library/[id]/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](src/lib/components/library/EpisodeFocusView.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### DR-063 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L39) + - **Line:** 39 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L50) + - **Line:** 50 + - **Context:** `export function scopeItemTypes(scope: SearchScope): string[] | undefin...` + +### DR-064 + +**Locations:** 3 file(s) + +- **File:** [`src/routes/library/+layout.svelte`](src/routes/library/+layout.svelte#L41) + - **Line:** 41 + - **Context:** `Unknown` +- **File:** [`src/routes/search/+page.svelte`](src/routes/search/+page.svelte#L15) + - **Line:** 15 + - **Context:** `Unknown` +- **File:** [`src/lib/components/search/SearchScopeChips.svelte`](src/lib/components/search/SearchScopeChips.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` + +### DR-065 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L235) + - **Line:** 235 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/librarySearchScope.test.ts`](src/lib/stores/librarySearchScope.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### DR-066 + +**Locations:** 5 file(s) + +- **File:** [`src/lib/components/settings/SearchGroupOrderList.svelte`](src/lib/components/settings/SearchGroupOrderList.svelte#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.ts`](src/lib/stores/searchGroupOrder.ts#L6) + - **Line:** 6 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.test.ts`](src/lib/stores/searchGroupOrder.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L117) + - **Line:** 117 + - **Context:** `Unknown` + +### DR-067 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/components/search/SearchResults.svelte`](src/lib/components/search/SearchResults.svelte#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L161) + - **Line:** 161 + - **Context:** `Unknown` + +### DR-068 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### DR-069 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### DR-070 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### DR-074 + +**Locations:** 17 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L938) + - **Line:** 938 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L949) + - **Line:** 949 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1854) + - **Line:** 1854 + - **Context:** `export type MergedMediaItem = { id: string; title: string; artist: str...` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1863) + - **Line:** 1863 + - **Context:** `export type NetworkStateWrapperArg = { networkType: NetworkType; unmet...` +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L81) + - **Line:** 81 + - **Context:** `Unknown` +- **File:** [`src/lib/services/networkType.ts`](src/lib/services/networkType.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L27) + - **Line:** 27 + - **Context:** `pub struct DownloadManagerWrapper(pub Mutex);` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L36) + - **Line:** 36 + - **Context:** `pub struct NetworkStateWrapper(pub NetworkStateHandle);` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L79) + - **Line:** 79 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L92) + - **Line:** 92 + - **Context:** `pub struct NetworkStateWrapperArg {` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1299) + - **Line:** 1299 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L22) + - **Line:** 22 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L42) + - **Line:** 42 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L70) + - **Line:** 70 + - **Context:** `impl NetworkState {` +- **File:** [`src-tauri/src/download/cache.rs`](src-tauri/src/download/cache.rs#L76) + - **Line:** 76 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/events.rs`](src-tauri/src/download/events.rs#L47) + - **Line:** 47 + - **Context:** `Unknown` + +### DR-075 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/account/AccountMenu.svelte`](src/lib/components/account/AccountMenu.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` + +### DR-076 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/AppHeader.svelte`](src/lib/components/AppHeader.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L80) + - **Line:** 80 + - **Context:** `Unknown` + +### DR-077 + +**Locations:** 2 file(s) + +- **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/viewMode.test.ts`](src/lib/stores/viewMode.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` + +### DR-078 + +**Locations:** 6 file(s) + +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/services/offlineCatalog.ts`](src/lib/services/offlineCatalog.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L24) + - **Line:** 24 + - **Context:** `Unknown` +- **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 + - **Context:** `Unknown` + +### DR-079 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### DR-080 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L43) + - **Line:** 43 + - **Context:** `Unknown` + +### DR-081 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` + +### DR-082 + +**Locations:** 16 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1211) + - **Line:** 1211 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1219) + - **Line:** 1219 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L97) + - **Line:** 97 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) + - **Line:** 105 + - **Context:** `Unknown` +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L203) + - **Line:** 203 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L218) + - **Line:** 218 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L143) + - **Line:** 143 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L154) + - **Line:** 154 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L531) + - **Line:** 531 + - **Context:** `Unknown` +- **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#L640) + - **Line:** 640 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) + - **Line:** 2623 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) + - **Line:** 2644 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) + - **Line:** 2663 + - **Context:** `Unknown` + +### DR-083 + +**Locations:** 14 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L795) + - **Line:** 795 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1219) + - **Line:** 1219 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) + - **Line:** 105 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) + - **Line:** 24 + - **Context:** `Unknown` +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L95) + - **Line:** 95 + - **Context:** `function sizeOf(itemId: string): number {` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1925) + - **Line:** 1925 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L218) + - **Line:** 218 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L143) + - **Line:** 143 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L531) + - **Line:** 531 + - **Context:** `Unknown` +- **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 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) + - **Line:** 2709 + - **Context:** `Unknown` + +### DR-085 + +**Locations:** 14 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1227) + - **Line:** 1227 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1699) + - **Line:** 1699 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L113) + - **Line:** 113 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L18) + - **Line:** 18 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L17) + - **Line:** 17 + - **Context:** `Unknown` +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/formatBytes.ts`](src/lib/utils/formatBytes.ts#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L235) + - **Line:** 235 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L161) + - **Line:** 161 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/types.rs`](src-tauri/src/repository/types.rs#L221) + - **Line:** 221 + - **Context:** `Unknown` +- **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 + - **Context:** `Unknown` + ### JA-001 **Locations:** 1 file(s) @@ -1300,12 +1731,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/sessions/RemoteControls.svelte`](src/lib/components/sessions/RemoteControls.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### JA-023 @@ -1314,12 +1745,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/sessions/RemoteControls.svelte`](src/lib/components/sessions/RemoteControls.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### JA-024 @@ -1342,12 +1773,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/lmsSync.ts`](src/lib/stores/lmsSync.ts#L12) - **Line:** 12 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### JA-026 @@ -1356,12 +1787,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/sessions/RemoteControls.svelte`](src/lib/components/sessions/RemoteControls.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### JA-028 @@ -1411,11 +1842,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 9 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1246) - - **Line:** 1246 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L178) - - **Line:** 178 +- **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 @@ -1423,11 +1851,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L411) - **Line:** 411 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L444) - - **Line:** 444 +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L203) + - **Line:** 203 - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L80) - - **Line:** 80 +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L494) + - **Line:** 494 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L435) - **Line:** 435 @@ -1438,13 +1866,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/online.rs`](src-tauri/src/repository/online.rs#L2125) - **Line:** 2125 - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L82) + - **Line:** 82 + - **Context:** `Unknown` ### UR-002 -**Locations:** 14 file(s) +**Locations:** 16 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1956) - - **Line:** 1956 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2098) + - **Line:** 2098 - **Context:** `Unknown` - **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) - **Line:** 10 @@ -1461,16 +1892,19 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) - - **Line:** 5 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) - **Line:** 136 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) +- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) + - **Line:** 5 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/schema.rs`](src-tauri/src/storage/schema.rs#L3) - **Line:** 3 - **Context:** `Unknown` - **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) @@ -1479,22 +1913,25 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - **Line:** 157 - **Context:** `pub fn file_size(&self) -> Option {` -- **File:** [`src-tauri/src/storage/schema.rs`](src-tauri/src/storage/schema.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` ### UR-003 **Locations:** 16 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1860) - - **Line:** 1860 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2002) + - **Line:** 2002 - **Context:** `export type PlaybackProgress = { itemId: string; positionTicks: number...` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1956) - - **Line:** 1956 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2098) + - **Line:** 2098 - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 @@ -1520,9 +1957,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - **Line:** 245 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L42) - **Line:** 42 - **Context:** `Unknown` @@ -1532,6 +1966,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` @@ -1543,11 +1980,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 13 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1860) - - **Line:** 1860 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2002) + - **Line:** 2002 - **Context:** `export type PlaybackProgress = { itemId: string; positionTicks: number...` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1956) - - **Line:** 1956 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2098) + - **Line:** 2098 - **Context:** `Unknown` - **File:** [`src/lib/components/player/AudioPlayer.svelte`](src/lib/components/player/AudioPlayer.svelte#L1) - **Line:** 1 @@ -1564,9 +2001,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - **Line:** 245 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L42) - **Line:** 42 - **Context:** `Unknown` @@ -1576,6 +2010,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` @@ -1587,14 +2024,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 54 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1974) - - **Line:** 1974 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2116) + - **Line:** 2116 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2179) - - **Line:** 2179 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2321) + - **Line:** 2321 - **Context:** `export type RemoteSessionStatus = { position: number; duration: number...` - **File:** [`src/lib/components/player/Controls.svelte`](src/lib/components/player/Controls.svelte#L1) - **Line:** 1 @@ -1647,12 +2084,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/utils/duration.test.ts`](src/lib/utils/duration.test.ts#L4) - **Line:** 4 - **Context:** `Unknown` -- **File:** [`src/lib/utils/layoutShell.test.ts`](src/lib/utils/layoutShell.test.ts#L14) - - **Line:** 14 - - **Context:** `Unknown` - **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L17) - **Line:** 17 - **Context:** `Unknown` +- **File:** [`src/lib/utils/layoutShell.test.ts`](src/lib/utils/layoutShell.test.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` - **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) - **Line:** 4 - **Context:** `Unknown` @@ -1698,6 +2135,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/index.ts`](src/lib/player/index.ts#L15) - **Line:** 15 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) + - **Line:** 6 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) + - **Line:** 23 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L42) - **Line:** 42 - **Context:** `Unknown` @@ -1713,11 +2156,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/player/backend.rs`](src-tauri/src/player/backend.rs#L58) - **Line:** 58 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L25) - - **Line:** 25 +- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) + - **Line:** 2 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L8) - **Line:** 8 @@ -1728,27 +2168,24 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - **Line:** 535 - **Context:** `pub enum AddPosition {` -- **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) - - **Line:** 6 +- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L7) + - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) - - **Line:** 23 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/session.rs`](src-tauri/src/commands/player/session.rs#L3) - - **Line:** 3 +- **File:** [`src-tauri/src/player/state.rs`](src-tauri/src/player/state.rs#L25) + - **Line:** 25 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - - **Line:** 2 +- **File:** [`src-tauri/src/commands/player/session.rs`](src-tauri/src/commands/player/session.rs#L3) + - **Line:** 3 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/conversions.rs`](src-tauri/src/commands/conversions.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` ### UR-007 @@ -1769,9 +2206,6 @@ 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/GenericMediaListPage.svelte`](src/lib/components/library/GenericMediaListPage.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src/lib/stores/movies.ts`](src/lib/stores/movies.ts#L3) - **Line:** 3 - **Context:** `Unknown` @@ -1781,6 +2215,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/music.ts`](src/lib/stores/music.ts#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` - **File:** [`src/lib/services/imageCache.test.ts`](src/lib/services/imageCache.test.ts#L4) - **Line:** 4 - **Context:** `Unknown` @@ -1895,18 +2332,18 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/playbackMode.test.ts`](src/lib/stores/playbackMode.test.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sessions.rs`](src-tauri/src/commands/sessions.rs#L1) - - **Line:** 1 +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/playback_mode.rs`](src-tauri/src/commands/playback_mode.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/sessions.rs`](src-tauri/src/commands/sessions.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` - **File:** [`src-tauri/src/session_poller/mod.rs`](src-tauri/src/session_poller/mod.rs#L6) - **Line:** 6 - **Context:** `Unknown` @@ -1915,8 +2352,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 10 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1956) - - **Line:** 1956 +- **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) - **Line:** 2 @@ -1933,8 +2370,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/player/media.rs`](src-tauri/src/player/media.rs#L117) - **Line:** 117 - **Context:** `pub enum MediaType {` -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - **Line:** 3 @@ -1989,14 +2426,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2 - **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-015 **Locations:** 13 file(s) @@ -2077,12 +2506,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) - - **Line:** 5 - - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) + - **Line:** 5 + - **Context:** `Unknown` ### UR-018 @@ -2094,16 +2523,16 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 - **Context:** `Unknown` ### UR-019 **Locations:** 12 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` - **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) - **Line:** 4 @@ -2189,8 +2618,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` - **File:** [`src/lib/stores/nextEpisode.ts`](src/lib/stores/nextEpisode.ts#L9) - **Line:** 9 @@ -2219,15 +2648,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L340) - **Line:** 340 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) - **Line:** 6 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) - **Line:** 23 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` @@ -2238,15 +2667,73 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` +### UR-024 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-025 + +**Locations:** 14 file(s) + +- **File:** [`src/lib/utils/validation.test.ts`](src/lib/utils/validation.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L21) + - **Line:** 21 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L89) + - **Line:** 89 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L122) + - **Line:** 122 + - **Context:** `Unknown` +- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L64) + - **Line:** 64 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L156) + - **Line:** 156 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/playback_reporting.rs`](src-tauri/src/commands/playback_reporting.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) + - **Line:** 5 + - **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 {` + ### UR-026 **Locations:** 16 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2031) - - **Line:** 2031 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2173) + - **Line:** 2173 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2221) - - **Line:** 2221 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2363) + - **Line:** 2363 - **Context:** `export type SessionInfo = { id?: string | null; userId?: string | null...` - **File:** [`src/lib/components/player/SleepTimerModal.svelte`](src/lib/components/player/SleepTimerModal.svelte#L1) - **Line:** 1 @@ -2269,9 +2756,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L301) - **Line:** 301 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` - **File:** [`src-tauri/src/player/sleep_timer.rs`](src-tauri/src/player/sleep_timer.rs#L4) - **Line:** 4 - **Context:** `Unknown` @@ -2284,6 +2768,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L23) - **Line:** 23 - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/autoplay.rs`](src-tauri/src/player/autoplay.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` @@ -2304,14 +2791,23 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-029 -**Locations:** 2 file(s) +**Locations:** 5 file(s) +- **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` - **File:** [`src/lib/components/library/GenericMediaListPage.svelte`](src/lib/components/library/GenericMediaListPage.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` - **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - **Line:** 2 - **Context:** `Unknown` +- **File:** [`src/lib/stores/viewMode.test.ts`](src/lib/stores/viewMode.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` ### UR-030 @@ -2361,8 +2857,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-035 -**Locations:** 4 file(s) +**Locations:** 5 file(s) +- **File:** [`src/routes/library/[id]/+page.svelte`](src/routes/library/[id]/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` - **File:** [`src/lib/components/library/CastSection.svelte`](src/lib/components/library/CastSection.svelte#L1) - **Line:** 1 - **Context:** `Unknown` @@ -2376,265 +2875,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` -### UR-040 - -**Locations:** 18 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#L1246) - - **Line:** 1246 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L178) - - **Line:** 178 - - **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/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - - **Line:** 4 - - **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#L543) - - **Line:** 543 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L623) - - **Line:** 623 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L444) - - **Line:** 444 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L80) - - **Line:** 80 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L435) - - **Line:** 435 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2084) - - **Line:** 2084 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2125) - - **Line:** 2125 - - **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#L543) - - **Line:** 543 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L623) - - **Line:** 623 - - **Context:** `Unknown` - -### UT-010 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UT-017 - -**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-018 - -**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-019 - -**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-021 - -**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-023 - -**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-024 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UT-060 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - - **Line:** 10 - - **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#L1246) - - **Line:** 1246 - - **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#L543) - - **Line:** 543 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L623) - - **Line:** 623 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L444) - - **Line:** 444 - - **Context:** `Unknown` - -### UR-024 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-025 - -**Locations:** 14 file(s) - -- **File:** [`src/lib/utils/validation.test.ts`](src/lib/utils/validation.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L21) - - **Line:** 21 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L89) - - **Line:** 89 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L122) - - **Line:** 122 - - **Context:** `Unknown` -- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L64) - - **Line:** 64 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L156) - - **Line:** 156 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) - - **Line:** 5 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/playback_reporting.rs`](src-tauri/src/commands/playback_reporting.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 {` - ### UR-036 **Locations:** 3 file(s) @@ -2649,6 +2889,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` +### UR-038 + +**Locations:** 1 file(s) + +- **File:** [`src/routes/library/[id]/+page.svelte`](src/routes/library/[id]/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + ### UR-039 **Locations:** 1 file(s) @@ -2657,6 +2905,68 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` +### UR-040 + +**Locations:** 19 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/api/repository-client.ts`](src/lib/api/repository-client.ts#L203) + - **Line:** 203 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **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` +- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) + - **Line:** 4 + - **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#L68) + - **Line:** 68 + - **Context:** `pub struct VideoSettingsWrapper(pub Mutex);` +- **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` +- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L435) + - **Line:** 435 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2084) + - **Line:** 2084 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2125) + - **Line:** 2125 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L82) + - **Line:** 82 + - **Context:** `Unknown` + ### UR-041 **Locations:** 1 file(s) @@ -2665,6 +2975,57 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` +### UR-043 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/connectivity.rs`](src-tauri/src/commands/connectivity.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-048 + +**Locations:** 2 file(s) + +- **File:** [`src/routes/library/[id]/+page.svelte`](src/routes/library/[id]/+page.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](src/lib/components/library/EpisodeFocusView.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-049 + +**Locations:** 8 file(s) + +- **File:** [`src/routes/library/+layout.svelte`](src/routes/library/+layout.svelte#L41) + - **Line:** 41 + - **Context:** `Unknown` +- **File:** [`src/routes/search/+page.svelte`](src/routes/search/+page.svelte#L15) + - **Line:** 15 + - **Context:** `Unknown` +- **File:** [`src/lib/components/search/SearchScopeChips.svelte`](src/lib/components/search/SearchScopeChips.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L235) + - **Line:** 235 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/librarySearchScope.test.ts`](src/lib/stores/librarySearchScope.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L39) + - **Line:** 39 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L50) + - **Line:** 50 + - **Context:** `export function scopeItemTypes(scope: SearchScope): string[] | undefin...` + ### IT-003 **Locations:** 1 file(s) @@ -2681,6 +3042,23 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **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) @@ -2705,24 +3083,16 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 535 - **Context:** `pub enum AddPosition {` -### UT-012 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/sleep_timer.rs`](src-tauri/src/player/sleep_timer.rs#L81) - - **Line:** 81 - - **Context:** `pub fn cancel(&mut self) {` - -### 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-027 @@ -2780,6 +3150,46 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **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) @@ -2821,6 +3231,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **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) @@ -2845,53 +3263,278 @@ 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-042 +### UR-050 -**Locations:** 1 file(s) +**Locations:** 7 file(s) -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src/lib/components/search/SearchResults.svelte`](src/lib/components/search/SearchResults.svelte#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/components/settings/SearchGroupOrderList.svelte`](src/lib/components/settings/SearchGroupOrderList.svelte#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.ts`](src/lib/stores/searchGroupOrder.ts#L6) + - **Line:** 6 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.test.ts`](src/lib/stores/searchGroupOrder.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L117) + - **Line:** 117 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L161) + - **Line:** 161 - **Context:** `Unknown` -### UT-043 +### UR-051 -**Locations:** 1 file(s) +**Locations:** 2 file(s) -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1841) - - **Line:** 1841 +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) + - **Line:** 1 - **Context:** `Unknown` -### UR-047 +### UR-052 -**Locations:** 1 file(s) +**Locations:** 9 file(s) -- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L245) + - **Line:** 245 + - **Context:** `Unknown` +- **File:** [`src/lib/services/offlineCatalog.ts`](src/lib/services/offlineCatalog.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L24) + - **Line:** 24 + - **Context:** `Unknown` +- **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 + - **Context:** `Unknown` -### UT-044 +### UR-053 + +**Locations:** 17 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L938) + - **Line:** 938 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L949) + - **Line:** 949 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1854) + - **Line:** 1854 + - **Context:** `export type MergedMediaItem = { id: string; title: string; artist: str...` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1863) + - **Line:** 1863 + - **Context:** `export type NetworkStateWrapperArg = { networkType: NetworkType; unmet...` +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L81) + - **Line:** 81 + - **Context:** `Unknown` +- **File:** [`src/lib/services/networkType.ts`](src/lib/services/networkType.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L27) + - **Line:** 27 + - **Context:** `pub struct DownloadManagerWrapper(pub Mutex);` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L36) + - **Line:** 36 + - **Context:** `pub struct NetworkStateWrapper(pub NetworkStateHandle);` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L79) + - **Line:** 79 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L92) + - **Line:** 92 + - **Context:** `pub struct NetworkStateWrapperArg {` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1299) + - **Line:** 1299 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L22) + - **Line:** 22 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L42) + - **Line:** 42 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/network.rs`](src-tauri/src/download/network.rs#L70) + - **Line:** 70 + - **Context:** `impl NetworkState {` +- **File:** [`src-tauri/src/download/cache.rs`](src-tauri/src/download/cache.rs#L76) + - **Line:** 76 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/download/events.rs`](src-tauri/src/download/events.rs#L47) + - **Line:** 47 + - **Context:** `Unknown` + +### UR-054 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/components/account/AccountMenu.svelte`](src/lib/components/account/AccountMenu.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/components/AppHeader.svelte`](src/lib/components/AppHeader.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L80) + - **Line:** 80 + - **Context:** `Unknown` + +### UR-055 + +**Locations:** 21 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L795) + - **Line:** 795 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1211) + - **Line:** 1211 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1219) + - **Line:** 1219 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L97) + - **Line:** 97 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) + - **Line:** 105 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) + - **Line:** 24 + - **Context:** `Unknown` +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L95) + - **Line:** 95 + - **Context:** `function sizeOf(itemId: string): number {` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L1925) + - **Line:** 1925 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L203) + - **Line:** 203 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L218) + - **Line:** 218 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L143) + - **Line:** 143 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L154) + - **Line:** 154 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L531) + - **Line:** 531 + - **Context:** `Unknown` +- **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#L640) + - **Line:** 640 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) + - **Line:** 2623 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) + - **Line:** 2644 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) + - **Line:** 2663 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) + - **Line:** 2709 + - **Context:** `Unknown` + +### UR-056 + +**Locations:** 15 file(s) + +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1227) + - **Line:** 1227 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1699) + - **Line:** 1699 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L113) + - **Line:** 113 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L18) + - **Line:** 18 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L17) + - **Line:** 17 + - **Context:** `Unknown` +- **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/formatBytes.ts`](src/lib/utils/formatBytes.ts#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/services/downloadedCatalog.ts`](src/lib/services/downloadedCatalog.ts#L95) + - **Line:** 95 + - **Context:** `function sizeOf(itemId: string): number {` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L235) + - **Line:** 235 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L161) + - **Line:** 161 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/types.rs`](src-tauri/src/repository/types.rs#L221) + - **Line:** 221 + - **Context:** `Unknown` +- **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 + - **Context:** `Unknown` + +### UT-010 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) - - **Line:** 136 +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 - **Context:** `Unknown` -### UR-042 +### UT-012 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-043 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/connectivity.rs`](src-tauri/src/commands/connectivity.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/sleep_timer.rs`](src-tauri/src/player/sleep_timer.rs#L81) + - **Line:** 81 + - **Context:** `pub fn cancel(&mut self) {` ### UT-014 @@ -2919,6 +3562,39 @@ 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-017 + +**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-018 + +**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-019 + +**Locations:** 2 file(s) + - **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - **Line:** 311 - **Context:** `Unknown` @@ -2937,7 +3613,7 @@ 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-025 +### UT-021 **Locations:** 2 file(s) @@ -2948,6 +3624,146 @@ 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-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-023 + +**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-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/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-042 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 + - **Context:** `Unknown` + +### UT-043 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 + - **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-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 + - **Context:** `Unknown` + +### UR-047 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UT-044 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) + - **Line:** 136 + - **Context:** `Unknown` + +### UT-046 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2623) + - **Line:** 2623 + - **Context:** `Unknown` + +### UT-047 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2644) + - **Line:** 2644 + - **Context:** `Unknown` + +### UT-048 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2663) + - **Line:** 2663 + - **Context:** `Unknown` + +### UT-049 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2681) + - **Line:** 2681 + - **Context:** `Unknown` + +### UT-051 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2709) + - **Line:** 2709 + - **Context:** `Unknown` + ### UT-059 **Locations:** 3 file(s) @@ -2962,4 +3778,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2125 - **Context:** `Unknown` +### UT-067 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2326) + - **Line:** 2326 + - **Context:** `Unknown` + diff --git a/docs/ux-flows.md b/docs/ux-flows.md index c708f49f..e20072ab 100644 --- a/docs/ux-flows.md +++ b/docs/ux-flows.md @@ -36,21 +36,74 @@ On desktop (md breakpoint and above), the header contains: - Logo (links to `/library`) - Navigation links: Home, Library, Downloads, Settings - Search bar (inline) -- User menu: Username, Downloads icon, Logout button +- Account menu (see §1.2) **Mobile Navigation:** On mobile, the header contains: - Logo -- Three-dot overflow menu button (Android-style) -- Overflow menu includes: - - Downloads - - Settings - - Sign out +- Account menu button (see §1.2) + +### 1.2 Account Menu + +Account-level destinations — the ones that are *about the user* rather than +about media — live behind a single **account menu**, anchored to the user's +name/avatar at the right of the header. + +**Contents, in order:** + +``` +┌──────────────────────────┐ +│ Signed in as │ ← identity, not a menu item +│ │ +├──────────────────────────┤ +│ ⬇ Downloads │ +│ ⚙ Settings │ +│ ▦ Display │ ← grid/list preference (§5A.2) +├──────────────────────────┤ +│ ⇥ Sign out │ +└──────────────────────────┘ +``` + +**Rules:** + +- **One menu, both platforms.** Desktop and mobile show the same items in the + same order. A user who learns where Settings lives on one form factor finds + it in the same place on the other. +- **Anchored to identity.** The trigger is the username/avatar, because that is + where users look for account actions. A bare three-dot icon does not signal + "your account". +- **Sign out is separated** by a divider and placed last — it is destructive and + must not sit adjacent to routine navigation. +- **The menu is reachable from every authenticated screen**, not only from + library routes. See §1.3. **Access Points Summary:** -- **Downloads** → Desktop: nav link + icon; Mobile: overflow menu -- **Settings** → Desktop: nav link; Mobile: overflow menu +- **Downloads** → header icon (desktop) + account menu (both) +- **Settings** → header nav link (desktop) + account menu (both) +- **Sign out** → account menu only + +### 1.3 Chrome availability + +The header is shared across chrome-bearing routes. Routes fall into three groups: + +| Route group | Header | Bottom nav | Account menu reachable? | +|-------------|--------|------------|-------------------------| +| `/library/*` | Yes (own layout, shared `AppHeader`) | Yes | Yes | +| `/`, `/search`, `/downloads` | Yes (root-owned `AppHeader`) | Yes | Yes | +| `/settings` | Own layout | No | n/a — already there | +| `/player/*`, `/login` | No | No | No (by design) | + +The rule the app honours: every authenticated, non-immersive screen exposes the +account menu. Only the full-screen player and the login screen are chrome-free. + +### 1.4 Known deviations + +*(None — the account-menu and chrome-availability defects tracked here under +UR-054 were resolved. Settings, Downloads, Display, and Sign out are now reachable +from every authenticated non-immersive screen via the shared `AccountMenu`, the +username/avatar is the menu trigger, desktop and mobile share one menu, and the +Display preference has a Settings entry — UR-029, §5A.4.)* --- @@ -386,9 +439,9 @@ flowchart TB ```mermaid flowchart TB - AlbumsGrid[Albums Grid
FORCED Grid View] --> UserAction{User Action} + AlbumsGrid[Albums Grid
grid/list per §5A] --> UserAction{User Action} - UserAction -->|Click Album| AlbumDetail[Album Detail Page
/library/[albumId]] + UserAction -->|Click Album| AlbumDetail[Album Detail Page
/library/[id]] UserAction -->|Click Play on Card| PlayAlbum[Play Album Immediately] AlbumDetail --> ShowAlbum[Show Album:
- Album Art
- Title, Artist
- Track List
- Download Button
- Favorite Button] @@ -445,54 +498,352 @@ flowchart TB --- -## 6. Search Flow +## 5A. Library Page Layouts -### 6.1 Search Page Navigation +Every browse page is one of two shapes: a **card grid** or a **row list**. This +section is the rule for which shape a page takes, what a card looks like, and +what the user is allowed to change. + +### 5A.1 Card shape follows the media, not the page + +Card aspect ratio is a property of *what the item is*, and is never overridden +per-page. This is the single most important layout rule: a user scanning a grid +recognises content type by silhouette before reading a word. + +| Item type | Aspect | Rationale | +|-----------|--------|-----------| +| Album, Artist, Track, Playlist | **1:1 square** | Matches album art; the universal music convention (Spotify) | +| Movie, Series, Season | **2:3 poster** | Matches printed poster art; the universal video convention (Netflix) | +| Episode | **16:9 thumbnail** | A frame from the episode, not cover art — signals "a thing you watch next" | +| Library / collection folder | **16:9** | Reads as a container, distinct from the items inside it | + +Artist cards are square but rendered **circular-masked**, so artists are +distinguishable from albums at a glance within the same music grid. + +### 5A.2 Grid vs. list ```mermaid flowchart TB - BottomNav[Bottom Nav] --> ClickSearch[Click Search Tab] + Page[Library browse page] --> Kind{Content kind} - ClickSearch --> SearchPage[Search Page
/search] + Kind -->|Visual-first
albums, artists, movies,
shows, playlists| Grid[Card grid
user may switch to list] + Kind -->|Ordinal
tracks in an album,
episodes in a season| List[Row list
always; no toggle] - SearchPage --> EmptyState{Has Query?} - - EmptyState -->|No| ShowPrompt[Show Empty State:
Search for music,
movies, shows...] - EmptyState -->|Yes| ShowResults[Show Results Grouped:
- Songs
- Albums
- Artists
- Movies
- Episodes] - - ShowPrompt --> UserTypes[User Types in Search] - UserTypes --> LiveSearch[Live Search
Debounced 300ms] - LiveSearch --> ShowResults - - ShowResults --> UserClick{User Clicks Result} - - UserClick -->|Song| PlaySong[Play Song + Queue Results] - UserClick -->|Album| NavAlbum[Navigate to Album Detail] - UserClick -->|Artist| NavArtist[Navigate to Artist Page] - UserClick -->|Movie| NavMovie[Navigate to Movie Detail] + Grid --> Toggle[View toggle in page header] + Toggle --> Persist[Choice persists globally
across all grid pages] ``` -**Search Page Layout:** +- **Grids are the default** for anything with cover art worth scanning. +- **Lists are mandatory, not optional**, where position carries meaning — + a track's number within an album, an episode's number within a season. + A grid destroys that ordering cue, so these pages expose **no toggle**. +- **The toggle is global, not per-page.** A user who prefers dense lists + prefers them everywhere; making them re-set it on each page is friction. + The choice persists across launches. + +**Responsive columns** (grid mode), tuned so cards stay large enough to read +cover art on a phone and don't become postage stamps on a desktop: + +| Breakpoint | Columns | +|------------|---------| +| base (phone) | 2 | +| sm | 3 | +| md | 4 | +| lg | 5 | +| xl | 6 | + +### 5A.3 What a card shows + +``` + ┌─────────────┐ + │ │ ← cover art (aspect per §5A.1) + │ artwork │ • progress bar overlay if partially played + │ │ • watched/played check if complete + │ [▶] │ • play affordance on hover/focus + └─────────────┘ + Primary line ← title, truncated to one line + Secondary line ← artist / year+rating / SxEy — one line, dimmed +``` + +- **Two lines of text maximum.** Titles truncate rather than wrap; a card that + grows to fit its title breaks grid alignment and makes scanning harder. +- **Progress and watched state live on the artwork**, not in the text — they + must be readable while scanning, without reading. +- **Hover/focus reveals play**, so a card is both a navigation target and a + playback target without a second control competing for space at rest. + +### 5A.4 Known deviations + +These are places the implementation currently diverges from the rules above. +They are recorded here so the gap is explicit rather than mistaken for intent. + +- **The view toggle is discoverable only on a browse page.** The preference is + already global and persisted, but the only control that sets it is the pair + of icon buttons in a library page header. Settings has no display section, so + there is nowhere to look for it. *(UR-029)* + +--- + +## 5B. Video Detail Page Composition + +Movie, Series, and Episode detail pages all live at `/library/[id]`. Which +surface renders is decided by item type plus the `?episode=` query param, and +**section order is part of the spec** — it is what makes "keep watching this +show" the path of least resistance. + +### 5B.1 Which surface renders + +```mermaid +flowchart TB + Nav[Navigate to /library/[id]] --> Type{Item type} + + Type -->|Person| Person[PersonDetailView] + Type -->|Movie| Movie[Movie detail
§5B.3] + Type -->|Series| Ep{?episode= param
present?} + + Ep -->|Yes| Focus[Episode Focus View
§5B.2] + Ep -->|No| Series[Series detail
§5B.4] + + Focus -->|Back to series| Series + Series -->|Click episode| Focus +``` + +An episode is **never** browsed as a bare `Episode` item page. Clicking an +episode anywhere navigates to `/library/?episode=`, so the +episode is always shown in the context of its series and the series' full +episode list is already loaded. + +### 5B.2 Episode Focus View — section order + +**The next episodes appear directly below the current episode, above cast and +similar shows.** Nothing may be inserted between the episode hero and the +episode strip. + +``` +┌─────────────────────────────────────────────────┐ +│ [←] │ +│ ┌───────────────────────────────────────────┐ │ +│ │ episode backdrop │ │ +│ │ Series Name │ │ ← 1. HERO +│ │ Episode Title │ │ +│ │ S2E4 • 48m • ★8.1 │ │ +│ │ Overview… │ │ +│ │ ▓▓▓▓▓░░░░░ 32m left │ │ +│ │ [▶ Play] │ │ +│ └───────────────────────────────────────────┘ │ +│ │ +│ More Episodes │ ← 2. EPISODE STRIP +│ ┌──────┐┌──────┐┌──────┐┌──────┐ │ (immediately below hero) +│ │ E3 ││▓E4▓ ││ E5 ││ E6 │ → scroll │ +│ │ ││NOW ││ ││ │ │ +│ └──────┘└──────┘└──────┘└──────┘ │ +│ │ +│ Cast │ ← 3. CAST +│ ( ○ )( ○ )( ○ )( ○ ) │ +│ │ +│ More Like This │ ← 4. SIMILAR +│ ┌────┐┌────┐┌────┐┌────┐ │ +└─────────────────────────────────────────────────┘ +``` + +**Rules for the episode strip:** + +- **Position is fixed.** Hero → episode strip → cast → similar. The strip sits + between the current episode and every other section; cast and related + content are *below* it, never above. +- **Window, not full list.** The strip shows a window around the current + episode — roughly 3 before and 6 after — so the immediate next episodes are + visible without scrolling, and earlier ones remain reachable by scrolling + left. It is horizontally scrollable, not a wrapped grid. +- **Forward bias.** More episodes are shown *after* the current one than + before it: the dominant intent on this screen is "watch the next one." +- **The current episode is present and marked.** It renders in-strip with a + "NOW" badge and a highlight ring, and is not clickable. It anchors the + user's position in the season rather than being hidden. +- **Cross-season continuity.** The window spans the whole series in episode + order, so the strip runs past a season boundary into the next season's first + episodes rather than dead-ending at the end of a season. +- **Per-episode state.** Each card shows a thumbnail, `SxEy` + title, a resume + progress bar when partially watched, and a watched checkmark when complete. +- **Clicking an episode swaps focus in place** (`?episode=` changes); it does + not start playback. Playback starts only from the hero's Play button. + +### 5B.3 Movie detail — section order + +``` +Hero (poster, title, metadata, Play / Download / Favorite) + → Crew links (Directed by / Written by / Music by) + → Genre tags + → Cast + → More Like This +``` + +A movie has no continuation set, so cast follows the hero directly. + +### 5B.4 Series detail — section order + +``` +Hero (poster, title, metadata, Play / Download) + → Crew links + → Genre tags + → Seasons + episodes (per-season sections) + → Cast + → More Like This +``` + +The same principle as §5B.2: **episodes come before cast and similar shows.** +The reason a user opens a series page is to pick an episode; discovery content +is secondary and sits underneath. + +--- + +## 6. Search Flow + +Search is **context-scoped**: what you are looking at when you start a search +determines what the search covers. A search begun inside the Music library +searches music. A search begun from Home or the top-level library page searches +everything. The scope is always shown, and always overridable. + +### 6.1 Scope is inherited from context + +```mermaid +flowchart TB + Start[User starts a search] --> Where{Where from?} + + Where -->|Home (/)| All[Scope: All] + Where -->|Library root (/library)| All + Where -->|Search tab| All + Where -->|Inside Music| Music[Scope: Music] + Where -->|Inside Movies| Movies[Scope: Movies] + Where -->|Inside TV| TV[Scope: TV] + + All --> Chips[Filter chips shown
All chip selected] + Music --> Chips2[Filter chips shown
Music chip preselected] + Movies --> Chips2 + TV --> Chips2 + + Chips --> Results[Results, grouped by type] + Chips2 --> Results + + Results --> Change{User taps a chip} + Change --> Rescope[Re-run search at new scope
query preserved] + Rescope --> Results +``` + +**Rules:** + +- **Context sets the *initial* chip, never a locked filter.** Entering search + from TV preselects the TV chip; the user can tap "All" to widen without + retyping the query. Scope is a starting point, not a cage. +- **Home, `/library`, and the search tab all start at "All".** These are the + places a user has expressed no narrower intent. +- **Changing scope preserves the query** and re-runs the search. Changing the + query preserves the scope. +- **Scope maps to item types**, resolved at the point of search: + + | Chip | `includeItemTypes` | + |------|--------------------| + | All | *(unset — every type)* | + | Music | `MusicAlbum`, `MusicArtist`, `Audio`, `Playlist` | + | Movies | `Movie` | + | TV | `Series`, `Episode` | + +- **Chips render under the search bar**, on both the dedicated search page and + the in-library header search. They are horizontally scrollable if they + overflow, never wrapped onto a second row. + +### 6.2 Search page layout + ``` ┌─────────────────────────────────────────┐ -│ [🔍 Search...] [✕] │ +│ [🔍 Search...] [✕] │ +│ │ +│ ( All ) (•Music•) ( Movies ) ( TV ) │ ← scope chips │ │ │ Songs ──────────────────────────── │ -│ ♪ Song Title - Artist 3:45 │ -│ ♪ Song Title - Artist 4:12 │ -│ See all (23) │ +│ ♪ Song Title - Artist 3:45 │ +│ ♪ Song Title - Artist 4:12 │ +│ See all (23) │ │ │ │ Albums ─────────────────────────── │ -│ [Album Cover] Album Title │ -│ [Album Cover] Album Title │ -│ See all (8) │ +│ [Cover] Album Title │ +│ See all (8) │ │ │ │ Artists ────────────────────────── │ -│ [Photo] Artist Name │ -│ See all (5) │ +│ ( Photo ) Artist Name │ +│ See all (5) │ └─────────────────────────────────────────┘ ``` +- Results stay **grouped by type** even when a scope is selected — a Music + search still separates Songs / Albums / Artists. +- Each group shows a bounded preview with a **See all (n)** affordance rather + than an unbounded list, so no single type can bury the others. +- Live search is **debounced** as the user types; a query that becomes empty + clears results rather than searching for the empty string. + +### 6.3 Result group order is user-configurable + +Which *kind* of thing a user is usually searching for is personal: a +music-first user wants Songs at the top, a TV-first user wants Shows. Rather +than guessing, the group order is a setting. + +```mermaid +flowchart TB + Settings[Settings → Search] --> List[Draggable list of result groups] + + List --> Drag[User drags a group up or down] + Drag --> Persist[Order persisted] + + Persist --> Render[Rendering a result set] + Scope[Active scope chip §6.1] --> Render + + Render --> Filter[1 - Drop groups outside the active scope] + Filter --> Sort[2 - Sort remaining groups by user order] + Sort --> Prune[3 - Omit groups with no results] + Prune --> Show[Render] +``` + +**Scope and order compose — they are two independent axes.** The scope chip +decides *which* groups are eligible; the settings list decides *what sequence* +the eligible ones appear in. Order is preserved as a relative ranking, never +renumbered per scope: + +- Scope **Music** with order `Movies → Songs → Albums → Artists → TV` renders + `Songs → Albums → Artists`. Movies and TV are filtered out; the surviving + groups keep their relative order. +- Scope **All** with the same setting renders all five in exactly that order. +- **Changing scope never rewrites the saved order.** A user who narrows to + Music and back to All sees their original arrangement intact. + +**Rules:** + +- **Drag and drop to reorder**, in a settings list showing every result group + (Songs, Albums, Artists, Movies, TV Shows). +- **The order applies to grouped results everywhere** — the search page and + the in-library header search alike. +- **Order is presentation-only.** It never changes which results are returned + or how they are ranked *within* a group, only the sequence groups appear in. +- **Empty groups are skipped, not gapped.** A group with no results is omitted + entirely; it does not reserve space or leave a stray heading. +- **A sensible default ships** (Songs → Albums → Artists → Movies → TV Shows) + so the setting is an adjustment, never a prerequisite. +- **Keyboard/accessible reordering must exist** alongside dragging — a + drag-only control is unusable with a screen reader or without a pointer. + +### 6.4 Known deviations + +Recorded so the gap between this spec and the build is explicit. + +- **Scope is not implemented.** The in-library header search calls the same + unscoped query as the global search page, so searching inside TV returns + music. The backend already accepts `includeItemTypes` on both the online and + offline paths, and the per-page list search already uses it — only the global + path ignores it. *(UR-049)* +- **Filter chips do not exist** on either search surface. *(UR-049)* +- **Group order is hardcoded** to Music → Movies → TV in the results markup, + with no setting. *(UR-050)* + --- ## 7. Download Flows @@ -532,68 +883,166 @@ States: 5. [⏸] Paused - Yellow pause icon ``` -### 7.2 Managing Downloads Page +### 7.2 Downloads = a browsable offline library, not a flat list + +**The central idea:** "my downloads" is not a list of file-transfer rows — it is +*the library, filtered to what's on the device*. A user who has downloaded three +seasons of a show and two albums thinks in terms of shows and albums, not +seventy-odd individual episode/track transfers. So the primary Downloads surface +**reuses the library browse screens**, scoped to downloaded content, and keeps +the transfer-progress list as a secondary "Transfers" view for the *act* of +downloading. + +This splits one overloaded page into two clear jobs: + +| Surface | Answers | Reuses | +|---------|---------|--------| +| **Downloaded** (browse) | "What do I have offline, and let me play it" | Library grids, detail pages, cards (§5A) | +| **Transfers** (activity) | "What is downloading right now, and control it" | The existing progress-row list | ```mermaid flowchart TB - User[User] --> NavChoice{Navigation Path} + Nav[Open Downloads] --> Downloads[/downloads] - NavChoice -->|Desktop| HeaderNav[Header: Click Downloads Link] - NavChoice -->|Mobile| HeaderIcon[Header: Click Downloads Icon] - NavChoice -->|Direct| TypeURL[Type /downloads] + Downloads --> View{View} + View -->|Downloaded (default)| Browse[Offline library browse] + View -->|Transfers| Activity[Transfer activity list] - HeaderNav --> DownloadsPage[Downloads Page
/downloads] - HeaderIcon --> DownloadsPage - TypeURL --> DownloadsPage + Browse --> Libs[Libraries — only those with
downloaded content] + Libs --> Grid[Library grid, offline-scoped
same cards/layout as online §5A] + Grid --> Detail[Detail page
same as online] + Detail --> Play[Play from local file] + Detail --> Remove[Remove download
frees space, keeps browsable? — see rules] - DownloadsPage --> ShowTabs[Show Tabs:
Active | Completed] - - ShowTabs --> ActiveTab{Active Tab} - - ActiveTab -->|Active| ShowActive[Show Active Downloads:
- Download progress bars
- Pause/Resume buttons
- Cancel buttons] - ActiveTab -->|Completed| ShowCompleted[Show Completed:
- Downloaded items list
- Delete buttons
- Play buttons] - - ShowActive --> UserAction1{User Action} - UserAction1 -->|Pause| PauseDownload[Pause Download] - UserAction1 -->|Cancel| CancelDialog[Show Confirm Dialog] - - ShowCompleted --> UserAction2{User Action} - UserAction2 -->|Play| PlayOffline[Play from Local File] - UserAction2 -->|Delete| DeleteDialog[Show Confirm Dialog] + Activity --> Rows[Per-transfer rows:
downloading / queued / paused / failed /
waiting-for-WiFi] + Rows --> Ctl[Pause / Resume / Cancel / Retry] ``` -**Navigation to Downloads:** -- **Desktop:** Click "Downloads" link in header navigation -- **All screen sizes:** Click download icon (⬇) button in header user menu -- **Direct:** Navigate to `/downloads` route +**Why reuse the library screens (not a bespoke list):** + +- **One mental model.** Browsing offline should feel identical to browsing + online — same grids, same card shapes, same detail pages, same play action. + The only difference is *what's present*, not *how it looks*. +- **It already works in the backend.** The offline repository's `get_items` + already returns downloaded items **plus** their containers (an album with any + downloaded track, a series/season with any downloaded episode). That is a + browsable tree today — see §7.4. +- **It scales.** A flat completed-list becomes unusable at a few dozen items; a + browsable library does not. + +### 7.3 The Downloaded browse surface -**Downloads Page Layout:** ``` -┌─────────────────────────────────────────┐ -│ [←] Downloads │ -│ │ -│ [Active (3)] [Completed (12)] │ -│ │ -│ ─ Downloading ──────────────────── │ -│ │ -│ Album Cover Album Title │ -│ Artist Name │ -│ [████████░░] 80% │ -│ [⏸ Pause] [✕ Cancel] │ -│ │ -│ Album Cover Album Title │ -│ Artist Name │ -│ [██░░░░░░░░] 20% │ -│ [⏸ Pause] [✕ Cancel] │ -│ │ -│ ─ Queued ───────────────────────── │ -│ │ -│ Album Cover Album Title │ -│ Artist Name │ -│ Waiting... │ -└─────────────────────────────────────────┘ +┌─────────────────────────────────────────────┐ +│ Downloads │ +│ ( Downloaded ) ( Transfers ) ← view switch +│ │ +│ [~ 3.4 GB on device · 12 items] Manage ▸ │ ← storage summary +│ │ +│ Music │ ← only libraries that +│ ┌────┐┌────┐┌────┐ │ have downloaded content +│ │alb ││alb ││art │ │ +│ └────┘└────┘└────┘ │ +│ │ +│ TV │ +│ ┌────┐┌────┐ │ +│ │show││show│ │ +│ └────┘└────┘ │ +└─────────────────────────────────────────────┘ ``` +**Rules:** + +- **Libraries with nothing downloaded are omitted**, not shown empty. If only + music is downloaded, only Music appears. +- **Cards, grids, and detail pages are the library's own** (§5A) — offline + browse is the same components with an offline-scoped data source, never a + parallel re-implementation. +- **A downloaded badge / "on device" affordance** distinguishes fully-downloaded + from partially-downloaded containers (e.g. a season with 6 of 10 episodes). +- **Disk usage is shown where the user already looks**, in familiar units — see + §7.3.1. +- **Play always plays the local file** here; nothing on this surface streams. +- **Remove is available at every level** — item, album/season, series — and + states clearly what it frees. Removing the last downloaded child of a + container removes the container from the browse. +- **This surface works identically online and offline.** It is "what's on the + device," a question whose answer does not depend on connectivity. It must not + wait for, or be emptied by, server reachability. + +#### 7.3.1 Disk usage — familiar, in place, not a separate audit + +Users want to know what each thing costs on disk, but that information has to +feel like the storage views they already know (phone Settings → Storage, a +file browser), not a developer's byte dump. + +- **Size rides along with the item, on the card and the detail page** — a small + secondary label (`1.2 GB`, `340 MB`, `48 MB`), never a separate "storage + report" screen the user has to go find. +- **Containers show their total.** A series shows the sum of its downloaded + episodes; an album the sum of its tracks; a season its own subtotal. The + number a user sees on the "Breaking Bad" card is what removing it frees. +- **Human units, rounded, consistent.** Binary or decimal is a choice — pick one + and use it everywhere. Show 2–3 significant figures (`1.2 GB`, not + `1,283,048,192 bytes` and not `1.28394 GB`). +- **A single device total sits at the top** of the Downloaded surface + (`3.4 GB on device · 12 items`) so the headline number is answered before the + user scans. It reconciles with the sum of what's listed. +- **Remove restates the reclaim** in the same units at the point of action + ("Remove download · frees 1.2 GB"), so the cost of keeping vs. freeing is + legible exactly when the user decides. +- **Sort/filter by size is a reasonable enhancement** ("biggest first" to find + what to clear) but is not required for v1. + +The bytes-on-disk per item are a backend fact (the download manager writes the +files and can stat them); this is a display and aggregation task, not new +tracking. See §7.7 deviations for what's missing today. + +### 7.4 Transfers (activity) view + +The existing progress-row list, unchanged in spirit, demoted to a secondary tab. +It is about *transfers in flight*, so it shows only rows that are doing or +waiting to do something: + +- **States:** downloading (with progress), queued, paused, failed, + waiting-for-WiFi (§7.5). +- **Controls:** Pause / Resume / Cancel / Retry per row; the 3-concurrent cap + and auto-pump are backend concerns and are not surfaced as manual controls. +- **Completed transfers fall off this view** once done — the finished item lives + in Downloaded, not here. A transient "just finished" confirmation is fine; a + permanent completed-list is not (that's what Downloaded is for). +- **Empty state** points at the library: "Nothing downloading. Browse your + library and tap download to save media for offline." + +### 7.5 Navigation & entry points + +- Reached via the account menu (§1.2) and, on desktop, the header Downloads + link/icon → `/downloads`. +- `/downloads` opens on **Downloaded** by default; **Transfers** is one tap away + and should draw attention (badge/count) only while transfers are active. +- Initiating a download is unchanged (§7.1): the download button lives on + item/album/series detail pages. The Downloads page manages and browses; it is + not where you start a download. + +### 7.7 Known deviations + +Recorded so the gap between this spec and the build is explicit. + +- **Downloads is a flat two-tab list today** (Active / Completed), rendering one + row per individual transfer with no browsing, grouping, or reuse of the + library screens. Completed downloads never collapse into their album/series. + *(UR-055)* +- **No offline-scoped browse entry point exists in the client.** All browsing + goes through the hybrid repository, which merges cache **and** server; there is + no way to ask for "downloaded content only" as a browse surface. The offline + repository supports it (§7.2) but is not reachable independently. *(UR-055, + DR-082)* +- **The "on device" storage summary and per-container remove** are absent from + the completed list. *(UR-055, UR-056)* +- **Per-item disk usage is not displayed anywhere.** Cards and detail pages show + no size; there is no device total, no container subtotal, and Remove does not + state what it frees. *(UR-056)* + --- ## 8. Settings & Account Flows