feat(series): land on the current episode, not season 1 (UR-062, UR-063, UR-064)
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 16m59s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m36s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
Build & Release / Run Tests (push) Successful in 5m9s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 8m54s
Build & Release / Build Linux (push) Successful in 18m49s
Build & Release / Build Windows (push) Successful in 14m4s
Build & Release / Build Android (push) Successful in 30m17s
Build & Release / Create Release (push) Successful in 18s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 16m59s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m36s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
Build & Release / Run Tests (push) Successful in 5m9s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 8m54s
Build & Release / Build Linux (push) Successful in 18m49s
Build & Release / Build Windows (push) Successful in 14m4s
Build & Release / Build Android (push) Successful in 30m17s
Build & Release / Create Release (push) Successful in 18s
Opening a series dumped the viewer at the top of season 1, and its Play button played nothing at all: it resolved `$libraryItems[0]` — the first *season* by SortName — and navigated to `/player/<seasonId>`, which the player route bounced straight back to `/library/<seasonId>`. The backend could already answer "where is this viewer in this show": `repository_get_next_up_episodes` has accepted a `series_id` since it was written and no caller had ever passed one. Backend (DR-101, DR-106) - `repository/series_progress.rs`: `pick_current_episode` — in progress, else Next Up, else first unwatched, else the premiere. The third rung is the offline path, where Next Up is always empty. `sort_series_order` puts specials (season 0) after the numbered seasons. - `repository_get_series_episodes` takes over the season fan-out and the flat-series fallback, which were domain knowledge living in the frontend. - `clear_watch_history` maps to Jellyfin's mark-unplayed (recursive over a container, also zeroes resume). Offline it refuses rather than diverging state the next sync would undo. Frontend (DR-102, DR-103, DR-104, DR-107) - Seasons collapse; only the current one is expanded, and the current episode is badged and scrolled into view. - Hero button reads `Resume S2E4` / `Play S1E1` and opens that episode's focus view, where Play commits (ux-flows §5B.5). - Seasons are no longer a destination: `/library/<seasonId>` redirects to `/library/<seriesId>#season-N`, and every inbound link follows. - The "More Episodes" strip spans the whole series, so a season finale offers the next premiere instead of dead-ending (§5B.2). - Clear-history buttons on the series hero and each season header. Routes (DR-105) - `/library/tv` and `/library/movies` absorb their all-titles and genres pages as `?view=` tabs; the four legacy routes redirect. 6 video routes become 2, and `/library/shows/genres` stops being the odd one out. Logic extracted to `seriesNavigation.ts`, `episodeStrip.ts` and `libraryView.ts` so it is unit-tested rather than buried in components. Spec: docs/specs/series-current-episode-navigation.md
This commit is contained in:
@@ -1267,6 +1267,46 @@ async repositoryGetResumeItems(handle: string, parentId: string | null, limit: n
|
||||
async repositoryGetNextUpEpisodes(handle: string, seriesId: string | null, limit: number | null) : Promise<MediaItem[]> {
|
||||
return await TAURI_INVOKE("repository_get_next_up_episodes", { handle, seriesId, limit });
|
||||
},
|
||||
/**
|
||||
* Every episode of a series, across all seasons, in series order.
|
||||
*
|
||||
* Jellyfin hangs episodes off season folders — except for "flat" series whose
|
||||
* children are episodes directly. Both shapes are provider vocabulary, so the
|
||||
* fan-out and its fallback live in Rust rather than being reimplemented in the
|
||||
* frontend (which is what it used to do).
|
||||
*
|
||||
* TRACES: UR-062 | DR-101
|
||||
*/
|
||||
async repositoryGetSeriesEpisodes(handle: string, seriesId: string) : Promise<MediaItem[]> {
|
||||
return await TAURI_INVOKE("repository_get_series_episodes", { handle, seriesId });
|
||||
},
|
||||
/**
|
||||
* The episode a viewer should land on when they open a series.
|
||||
*
|
||||
* "Current" is domain policy, not layout: an episode in progress, else the
|
||||
* server's Next Up for the series, else the first unwatched episode, else the
|
||||
* first. The third rung is what makes this work offline, where Next Up is
|
||||
* always empty. Returns `None` only when the series has no episodes at all.
|
||||
*
|
||||
* TRACES: UR-062 | DR-101
|
||||
*/
|
||||
async repositoryGetSeriesCurrentEpisode(handle: string, seriesId: string) : Promise<MediaItem | null> {
|
||||
return await TAURI_INVOKE("repository_get_series_current_episode", { handle, seriesId });
|
||||
},
|
||||
/**
|
||||
* Erase the viewer's watch history for an item.
|
||||
*
|
||||
* Clears the played flag and the resume position; on a series or season the
|
||||
* server applies it to everything inside. A series cleared this way is "never
|
||||
* watched" again, so `repository_get_series_current_episode` returns its
|
||||
* premiere. Requires the server — offline this fails rather than diverging
|
||||
* local state the next sync would overwrite.
|
||||
*
|
||||
* TRACES: UR-064 | DR-106
|
||||
*/
|
||||
async repositoryClearWatchHistory(handle: string, itemId: string) : Promise<null> {
|
||||
return await TAURI_INVOKE("repository_clear_watch_history", { handle, itemId });
|
||||
},
|
||||
/**
|
||||
* Get recently played audio
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user