diff --git a/docs/requirements.md b/docs/requirements.md index 5eb77ec1..d23fb9ba 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -71,6 +71,7 @@ For a narrative overview of the system design, see | UR-058 | On the home screen, a tap on a media card opens the item (movie/episode detail page, or the series Episode Focus View for episodes) rather than starting playback; a long-press starts "play now" after a confirm; an episode detail/focus page links back to its parent series and season (see [ux-flows.md §5B.5](ux-flows.md) and [§5B.1](ux-flows.md)) | Medium | Done | | UR-059 | Skipping to the next episode records the episode left behind as **fully watched** rather than saving a mid-episode resume point — skipping means "done with this one", not "stopped here" — and Continue Watching hides episodes the viewer has already moved past (a partial position behind that series' next-up episode), so the row only ever offers genuinely unfinished media | Medium | Done | | UR-060 | Search results are ordered by how well they match: a name that *starts* with the query outranks one matching mid-word (typing "parks" finds "Parks and Recreation" before "Sparks of Love"), and at equal match quality a container outranks its contents (a series before its episodes). Results are grouped into distinct categories — TV Shows, Episodes, Movies, Songs, Albums, Artists and People — so a show never competes with its own episodes for the same slot, and searching an actor's name reaches their bio | High | Done | +| UR-061 | Double tapping the video skips within it — right half jumps **forward 30 seconds**, left half jumps **back 10 seconds** — with an on-screen indicator naming the amount. Because a double tap starts as a single tap, the single-tap play/pause is held back until the double-tap window has passed, so skipping never also pauses the video; the skip lands relative to the position the player actually reports, and repeated double taps accumulate rather than all skipping from the same spot | Medium | Done | --- @@ -245,6 +246,7 @@ Internal architecture, components, and application logic. | DR-089 | Continue Watching suppresses resume entries superseded by Next Up: an in-progress episode whose series has a next-up entry strictly later in series order (season, then episode) is dropped from the Home and TV rows; movies, series without a next-up entry, and items with unknown/mixed episode ordering are always kept | UI | UR-059 | Done | | DR-090 | Relevance ranking in Rust (`domain/search_rank.rs`): results sort by match position (prefix → word-start → mid-word substring → no name match) then by media kind (containers before their contents), stably so the backend's own relevance breaks ties. Applied in `repository_search` to both the instant cache result and the merged cache+server union, so the list does not reshuffle when server results land | Backend | UR-060 | Done | | DR-091 | Search result groups split TV into separate Shows and Episodes groups and add a People group (default order: Shows → Episodes → Movies → Songs → Albums → Artists → People); a stored `tvShows` order from before the split expands in place to shows+episodes so an upgrading user keeps their arrangement | UI | UR-060 | Done | +| DR-092 | Video tap gestures resolve in `tapGestures.ts` (pure, unit-tested) rather than inline in `VideoPlayer.svelte`: `registerTap` returns `pending` for a first tap — the component defers `togglePlayPause` behind a `DOUBLE_TAP_WINDOW_MS` (300 ms) timer that a second tap cancels — or `seek` (+30 s right / −10 s left) for a second tap inside the window; a consumed second tap resets the state so a third tap starts fresh, and a swipe cancels the pending tap. The compatibility `click` the browser synthesizes after a touch tap is filtered in `handleVideoClick` so it cannot bypass the deferral. `resolveSeekTarget` converts the delta to the absolute position the facade requires, clamped to `[0, duration]` and chained off a still-in-flight `pendingSeekTarget` so back-to-back skips accumulate instead of all resolving against a not-yet-updated position | UI | UR-061 | Done | --- @@ -313,6 +315,7 @@ Internal architecture, components, and application logic. | UR-057 | - | DR-086 | | UR-058 | - | DR-087 | | UR-060 | - | DR-090, DR-091 | +| UR-061 | - | DR-092 | --- @@ -383,6 +386,7 @@ 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-062 | `setBackgroundAudioEnabled` reports whether the native bridge was actually reached (missing bridge, stale proxy, throwing method) so a dead bridge cannot look armed | UR-040, IR-025, DR-051 | Done | | 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 | Done | | 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 | Done | @@ -402,6 +406,10 @@ Internal architecture, components, and application logic. | UT-082 | EQ fields serialize as camelCase (`equalizerEnabled`/`equalizerBands`) and round-trip | DR-030 | Done | | UT-083 | EQ filter entries are empty when disabled or when the curve is flat (clears the `af` filter) | IR-020 | Done | | UT-084 | Enabled EQ builds one peaking `equalizer` per non-zero band at the right frequency and gain inside a single `lavfi` chain | IR-020 | Done | +| UT-085 | A first tap resolves to `pending`, not an immediate play/pause, and becomes `togglePlayPause` only once the double-tap window has elapsed | DR-092 | Done | +| UT-086 | A second tap inside the window seeks (+30 s right half, −10 s left half) with the matching feedback side, and clears the deferred play/pause so a double tap never pauses | DR-092 | Done | +| UT-087 | A tap after the window, and a third tap after a consumed double tap, each start a fresh pending tap; repeated double taps keep seeking; `cancel()` drops a pending tap so a swipe cannot pause | DR-092 | Done | +| UT-088 | `resolveSeekTarget` applies the delta to the reported position, clamps to `[0, duration]`, chains off an in-flight pending target so rapid skips accumulate, and ignores that target once the player reports past it | DR-092 | Done | ### Integration Tests diff --git a/docs/traceability.md b/docs/traceability.md index 916d8611..e4b99217 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,22 +1,22 @@ # Code Traceability Matrix -**Generated:** 7/25/2026, 3:13:47 PM +**Generated:** 7/28/2026, 1:33:13 AM ## Summary -- **Total Files Scanned:** 296 -- **Total TRACES Found:** 310 +- **Total Files Scanned:** 299 +- **Total TRACES Found:** 313 - **Requirements Covered:** - - User Requirements (UR): 57 + - User Requirements (UR): 58 - Integration Requirements (IR): 15 - - Development Requirements (DR): 83 + - Development Requirements (DR): 84 - 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-027, UR-028, UR-029, UR-030, UR-031, UR-032, UR-033, UR-034, UR-035, UR-036, UR-038, UR-039, UR-040, UR-041, UR-042, UR-043, UR-044, UR-045, UR-046, UR-047, UR-048, UR-049, UR-050, UR-051, UR-052, UR-053, UR-054, UR-055, UR-056, UR-057, UR-058, UR-059, UR-060 +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-027, UR-028, UR-029, UR-030, UR-031, UR-032, UR-033, UR-034, UR-035, UR-036, UR-038, UR-039, UR-040, UR-041, UR-042, UR-043, UR-044, UR-045, UR-046, UR-047, UR-048, UR-049, UR-050, UR-051, UR-052, UR-053, UR-054, UR-055, UR-056, UR-057, UR-058, UR-059, UR-060, UR-061 ``` ### 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-020, ### 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-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091 +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-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091, DR-092 ``` ### Jellyfin API Requirements (JA) @@ -192,11 +192,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### IR-025 -**Locations:** 1 file(s) +**Locations:** 2 file(s) - **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - **Line:** 4 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` ### IR-026 @@ -792,7 +795,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-029 -**Locations:** 9 file(s) +**Locations:** 10 file(s) - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2547) - **Line:** 2547 @@ -818,6 +821,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/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#L257) + - **Line:** 257 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -1169,7 +1175,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-051 -**Locations:** 2 file(s) +**Locations:** 3 file(s) - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 @@ -1177,6 +1183,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - **Line:** 4 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` ### DR-052 @@ -1200,8 +1209,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1079) - - **Line:** 1079 +- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1105) + - **Line:** 1105 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L68) - **Line:** 68 @@ -1816,6 +1825,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 183 - **Context:** `Unknown` +### DR-092 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + ### JA-001 **Locations:** 1 file(s) @@ -2280,7 +2300,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-005 -**Locations:** 59 file(s) +**Locations:** 60 file(s) - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2251) - **Line:** 2251 @@ -2303,6 +2323,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/player/videoFit.ts`](src/lib/components/player/videoFit.ts#L7) - **Line:** 7 - **Context:** `Unknown` +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` @@ -2833,46 +2856,9 @@ 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 {` -### UR-020 - -**Locations:** 4 file(s) - -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - - **Line:** 19 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) - - **Line:** 15 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-021 - -**Locations:** 5 file(s) - -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - - **Line:** 19 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) - - **Line:** 15 - - **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/storage/series_prefs.rs`](src-tauri/src/commands/storage/series_prefs.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - ### UR-023 -**Locations:** 20 file(s) +**Locations:** 21 file(s) - **File:** [`src/routes/library/movies/+page.svelte`](src/routes/library/movies/+page.svelte#L1) - **Line:** 1 @@ -2925,8 +2911,11 @@ 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/mod.rs`](src-tauri/src/player/mod.rs#L1079) - - **Line:** 1079 +- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L257) + - **Line:** 257 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1105) + - **Line:** 1105 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 @@ -3001,7 +2990,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-026 -**Locations:** 16 file(s) +**Locations:** 17 file(s) - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2308) - **Line:** 2308 @@ -3048,6 +3037,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/mod.rs`](src-tauri/src/player/mod.rs#L257) + - **Line:** 257 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -3240,7 +3232,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-040 -**Locations:** 21 file(s) +**Locations:** 22 file(s) - **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) - **Line:** 434 @@ -3272,11 +3264,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - **Line:** 4 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) + - **Line:** 7 + - **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/player/mod.rs`](src-tauri/src/player/mod.rs#L1079) - - **Line:** 1079 +- **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1105) + - **Line:** 1105 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L68) - **Line:** 68 @@ -3692,22 +3687,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 37 - **Context:** `Unknown` -### IT-003 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - -### IT-004 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - ### IT-013 **Locations:** 4 file(s) @@ -3725,30 +3704,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 675 - **Context:** `Unknown` -### UT-003 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - -### UT-004 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - -### UT-005 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` - ### UT-010 **Locations:** 1 file(s) @@ -3757,14 +3712,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` -### 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-024 **Locations:** 1 file(s) @@ -3784,26 +3731,10 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 251 - **Context:** `Unknown` -### UT-027 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - ### UT-028 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-029 - -**Locations:** 1 file(s) - - **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - **Line:** 245 - **Context:** `Unknown` @@ -3816,15 +3747,7 @@ 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-031 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-033 +### UT-032 **Locations:** 1 file(s) @@ -3906,12 +3829,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 9 - **Context:** `Unknown` -### UT-066 +### UR-061 -**Locations:** 1 file(s) +**Locations:** 2 file(s) -- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) - - **Line:** 4 +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 - **Context:** `Unknown` ### UT-071 @@ -3922,6 +3848,47 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` +### UT-085 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L704) + - **Line:** 704 + - **Context:** `Unknown` + +### UT-086 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L714) + - **Line:** 714 + - **Context:** `Unknown` + +### UT-087 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L759) + - **Line:** 759 + - **Context:** `Unknown` + +### UT-088 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` + ### UR-014 **Locations:** 1 file(s) @@ -3930,15 +3897,41 @@ 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-016 +### UR-020 -**Locations:** 2 file(s) +**Locations:** 4 file(s) -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L62) - - **Line:** 62 +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) - - **Line:** 251 +- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) + - **Line:** 19 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) + - **Line:** 15 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-021 + +**Locations:** 5 file(s) + +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) + - **Line:** 19 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) + - **Line:** 15 + - **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/storage/series_prefs.rs`](src-tauri/src/commands/storage/series_prefs.rs#L3) + - **Line:** 3 - **Context:** `Unknown` ### UR-060 @@ -3955,6 +3948,22 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 106 - **Context:** `Unknown` +### IT-003 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + +### IT-004 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + ### IT-016 **Locations:** 1 file(s) @@ -3971,6 +3980,38 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 441 - **Context:** `Unknown` +### UT-003 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + +### UT-004 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + +### UT-005 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` + +### UT-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-014 **Locations:** 2 file(s) @@ -4092,7 +4133,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-032 +### UT-027 **Locations:** 1 file(s) @@ -4100,6 +4141,109 @@ 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-029 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-031 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-033 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **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-051 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2900) + - **Line:** 2900 + - **Context:** `Unknown` + +### UT-059 + +**Locations:** 3 file(s) + +- **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#L2109) + - **Line:** 2109 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2150) + - **Line:** 2150 + - **Context:** `Unknown` + +### UT-062 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) + - **Line:** 7 + - **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-016 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L62) + - **Line:** 62 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) + - **Line:** 251 + - **Context:** `Unknown` + +### UR-031 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-032 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + ### UR-033 **Locations:** 7 file(s) @@ -4134,26 +4278,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2027 - **Context:** `Unknown` -### UT-051 +### UT-044 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2900) - - **Line:** 2900 - - **Context:** `Unknown` - -### UT-059 - -**Locations:** 3 file(s) - -- **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#L2109) - - **Line:** 2109 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2150) - - **Line:** 2150 +- **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) + - **Line:** 136 - **Context:** `Unknown` ### UT-067 @@ -4268,30 +4398,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 685 - **Context:** `Unknown` -### UT-085 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L704) - - **Line:** 704 - - **Context:** `Unknown` - -### UT-086 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L714) - - **Line:** 714 - - **Context:** `Unknown` - -### UT-087 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L759) - - **Line:** 759 - - **Context:** `Unknown` - ### UR-022 **Locations:** 1 file(s) @@ -4300,27 +4406,13 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 3 - **Context:** `Unknown` -### UR-031 +### UR-042 -**Locations:** 2 file(s) +**Locations:** 1 file(s) -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) +- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-032 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` ### UR-044 @@ -4346,14 +4438,6 @@ 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 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) - - **Line:** 2027 - - **Context:** `Unknown` - ### UR-047 **Locations:** 1 file(s) @@ -4362,20 +4446,4 @@ 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-044 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/offline.rs`](src-tauri/src/commands/offline.rs#L136) - - **Line:** 136 - - **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` - diff --git a/docs/ux-flows.md b/docs/ux-flows.md index 1ce04e62..9b93f038 100644 --- a/docs/ux-flows.md +++ b/docs/ux-flows.md @@ -346,10 +346,12 @@ flowchart TB **User Interaction:** - **Tap screen:** Controls reappear for 3 seconds - **Double tap left side:** Rewind 10 seconds (shows animated feedback with "-10" indicator) -- **Double tap right side:** Forward 10 seconds (shows animated feedback with "+10" indicator) +- **Double tap right side:** Forward 30 seconds (shows animated feedback with "+30" indicator) +- **Single tap play/pause is deferred** by the 300 ms double-tap window, so a double tap + skips without also toggling pause (UR-061) - **Swipe up/down on left side:** Adjust brightness (0.3-1.7x, shows brightness indicator with progress bar) - **Swipe up/down on right side:** Adjust volume (0-100%, shows volume indicator with progress bar) -- **Keyboard arrows:** ← rewind 10s, → forward 10s (desktop/external keyboard) +- **Keyboard arrows:** ← rewind 10s, → forward 30s (desktop/external keyboard) - **Keyboard space/K:** Toggle play/pause - **Keyboard F:** Toggle fullscreen - **Pinch:** Zoom (planned) diff --git a/package.json b/package.json index dd26f88b..9c47d91f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellytau", - "version": "0.1.2", + "version": "0.1.5", "description": "", "type": "module", "packageManager": "bun@1.3.5", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7c0f55d7..b8db9c6d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1994,7 +1994,7 @@ dependencies = [ [[package]] name = "jellytau" -version = "0.1.2" +version = "0.1.5" dependencies = [ "aes-gcm", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0c8ee6e4..58af9a77 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellytau" -version = "0.1.2" +version = "0.1.5" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/android/src/main/java/com/dtourolle/jellytau/MainActivity.kt b/src-tauri/android/src/main/java/com/dtourolle/jellytau/MainActivity.kt index 469e7fbb..fe5f37b7 100644 --- a/src-tauri/android/src/main/java/com/dtourolle/jellytau/MainActivity.kt +++ b/src-tauri/android/src/main/java/com/dtourolle/jellytau/MainActivity.kt @@ -1,10 +1,5 @@ package com.dtourolle.jellytau -import android.content.Context -import android.media.AudioAttributes -import android.media.AudioFocusRequest -import android.media.AudioManager -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -19,8 +14,6 @@ class MainActivity : TauriActivity() { private val handler = Handler(Looper.getMainLooper()) private var configAttempts = 0 private val maxConfigAttempts = 10 - private var audioFocusRequest: AudioFocusRequest? = null - private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } /** * Coarse override for whether backgrounding the app should auto-enter PiP. @@ -50,6 +43,15 @@ class MainActivity : TauriActivity() { */ private var mediaWebView: WebView? = null + /** + * The WebView the @JavascriptInterface bridges have been injected into. + * + * addJavascriptInterface must run once per WebView instance: re-injecting + * over an already-loaded page hands JS a stale proxy whose methods are gone. + * Compared by identity so a genuinely new WebView still gets its bridges. + */ + private var bridgesInstalledOn: WebView? = null + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -159,19 +161,36 @@ class MainActivity : TauriActivity() { android.util.Log.d("MainActivity", "WebView found! Configuring settings...") mediaWebView = webView - // Add JavaScript interface for audio focus control - webView.addJavascriptInterface(object : Any() { - @JavascriptInterface - fun requestAudioFocus() { - handler.post { this@MainActivity.requestAudioFocus() } - } + // Register the @JavascriptInterface bridges EXACTLY ONCE per WebView. + // + // configureWebViewForMedia() runs from onCreate's delayed post AND from + // every onResume (plus each WebView re-find), so this used to re-inject + // all four bridges repeatedly - 5 times in a 45s session. WebView binds + // injected objects at page-load time; re-injecting over a live page + // leaves JS holding a stale proxy. The object stays truthy while its + // methods vanish, which surfaced as a flood of + // "WebView: Unknown object" chromium errors and, in JS, + // "TypeError: setEnabled is not a function". + // + // The visible bug: the background-audio toggle turned blue but never + // reached native, so backgroundAudioEnabled stayed false, onStop never + // dispatched 'jellytau-background', and a locked screen killed audio + // instantly (UR-040). Audio focus and PiP broke the same way. + // + // The settings/WebChromeClient work below is idempotent and must keep + // running on resume; only the bridge injection is one-shot. + if (webView === bridgesInstalledOn) { + android.util.Log.d("MainActivity", "JS bridges already installed on this WebView - skipping re-injection") + configureWebViewSettings(webView) + return + } + bridgesInstalledOn = webView - @JavascriptInterface - fun abandonAudioFocus() { - handler.post { this@MainActivity.abandonAudioFocus() } - } - }, "AndroidAudioFocus") - android.util.Log.d("MainActivity", "JavaScript interface 'AndroidAudioFocus' added") + // NOTE: there is deliberately no "AndroidAudioFocus" bridge. Manual focus + // requests from the WebView competed with Chromium's own + // AudioFocusDelegate and with ExoPlayer, and the resulting + // AUDIOFOCUS_LOSS paused playback. See the comment on the video listeners + // in configureWebViewSettings(). // Add JavaScript interface for picture-in-picture control. // enterPip/canEnterPip must run on the main thread; @JavascriptInterface @@ -212,10 +231,6 @@ class MainActivity : TauriActivity() { backgroundAudioEnabled = enabled android.util.Log.d("MainActivity", "backgroundAudioEnabled = $enabled") } - - /** Whether background audio is available on this device (needs PiP-era APIs unnecessary; audio service always present on Android). */ - @JavascriptInterface - fun isSupported(): Boolean = true }, "AndroidBackgroundAudio") android.util.Log.d("MainActivity", "JavaScript interface 'AndroidBackgroundAudio' added") @@ -248,6 +263,21 @@ class MainActivity : TauriActivity() { dispatchWebEvent("jellytau-network-changed") } + configureWebViewSettings(webView) + + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Failed to configure WebView for media", e) + } + } + + /** + * WebView settings, chrome client and the video-unmute script. + * + * Split out from the bridge injection because this half is idempotent and + * must re-run on every resume, whereas addJavascriptInterface must not. + */ + private fun configureWebViewSettings(webView: WebView) { + try { // Set WebChromeClient to handle video playback and audio focus webView.webChromeClient = object : WebChromeClient() { override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { @@ -259,6 +289,21 @@ class MainActivity : TauriActivity() { super.onHideCustomView() android.util.Log.d("MainActivity", "Video exited fullscreen") } + + /** + * Forward WebView console output to logcat under the "JellyTauWeb" tag. + * + * Without this the frontend is invisible to `adb logcat`, which makes + * diagnosing anything that spans the JS/native boundary (the + * background-audio handoff in particular) guesswork. + */ + override fun onConsoleMessage(msg: android.webkit.ConsoleMessage): Boolean { + android.util.Log.d( + "JellyTauWeb", + "${msg.message()} (${msg.sourceId()}:${msg.lineNumber()})" + ) + return true + } } android.util.Log.d("MainActivity", "WebChromeClient configured") @@ -287,29 +332,18 @@ class MainActivity : TauriActivity() { video.volume = 1.0; console.log('[Android] Video unmuted, volume:', video.volume, 'muted:', video.muted); - // Add event listeners to manage audio focus - video.addEventListener('play', function() { - console.log('[Android] Video play event - requesting audio focus'); - if (typeof AndroidAudioFocus !== 'undefined') { - AndroidAudioFocus.requestAudioFocus(); - } - console.log('[Android] Video state - muted:', this.muted, 'volume:', this.volume); - }); - - video.addEventListener('pause', function() { - console.log('[Android] Video pause event - abandoning audio focus'); - if (typeof AndroidAudioFocus !== 'undefined') { - AndroidAudioFocus.abandonAudioFocus(); - } - }); - - video.addEventListener('ended', function() { - console.log('[Android] Video ended event - abandoning audio focus'); - if (typeof AndroidAudioFocus !== 'undefined') { - AndroidAudioFocus.abandonAudioFocus(); - } - }); - + // NOTE: deliberately no audio-focus calls here. + // + // WebView already manages audio focus for