🏗️ 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
95 lines
2.9 KiB
Svelte
95 lines
2.9 KiB
Svelte
<!--
|
|
Erase watch history for a series or a season.
|
|
|
|
The backend does the work (`repository_clear_watch_history` → Jellyfin's
|
|
mark-unplayed, which is recursive over a container and also zeroes resume
|
|
positions); this only confirms the intent and reports the outcome. Clearing a
|
|
series returns it to "never watched", so it reopens on S1E1.
|
|
|
|
TRACES: UR-064 | DR-106
|
|
-->
|
|
<script lang="ts">
|
|
import { auth } from "$lib/stores/auth";
|
|
import { isServerReachable } from "$lib/stores/connectivity";
|
|
|
|
interface Props {
|
|
/** Series or season id to clear. */
|
|
itemId: string;
|
|
/** Name shown in the confirm prompt. */
|
|
itemName: string;
|
|
/** What is being cleared, for the prompt wording. */
|
|
scope: "series" | "season";
|
|
size?: "sm" | "lg";
|
|
/** Called after a successful clear so the caller can reload. */
|
|
onCleared?: () => void;
|
|
}
|
|
|
|
let { itemId, itemName, scope, size = "lg", onCleared }: Props = $props();
|
|
|
|
let busy = $state(false);
|
|
|
|
const label = $derived(scope === "series" ? "Clear history" : "Clear season history");
|
|
|
|
async function handleClick() {
|
|
if (busy) return;
|
|
|
|
const subject = scope === "series" ? `all of “${itemName}”` : `“${itemName}”`;
|
|
// Destructive and not undoable — always ask, even though the server keeps
|
|
// no undo of its own.
|
|
if (
|
|
!confirm(
|
|
`Erase watch history for ${subject}?\n\n` +
|
|
"Every episode is marked unwatched and resume positions are cleared. " +
|
|
"This cannot be undone."
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
busy = true;
|
|
try {
|
|
await auth.getRepository().clearWatchHistory(itemId);
|
|
onCleared?.();
|
|
} catch (e) {
|
|
console.error("Failed to clear watch history:", e);
|
|
alert(
|
|
`Could not clear watch history: ${e instanceof Error ? e.message : String(e)}`
|
|
);
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<button
|
|
onclick={handleClick}
|
|
disabled={busy || !$isServerReachable}
|
|
title={$isServerReachable
|
|
? "Mark everything unwatched and clear resume positions"
|
|
: "Needs a connection to the server"}
|
|
class="rounded-lg font-medium flex items-center gap-2 transition-colors
|
|
bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)]
|
|
disabled:opacity-40 disabled:cursor-not-allowed
|
|
{size === 'lg' ? 'px-6 py-2' : 'px-3 py-1.5 text-sm'}"
|
|
>
|
|
{#if busy}
|
|
<div
|
|
class="border-2 border-current border-t-transparent rounded-full animate-spin
|
|
{size === 'lg' ? 'w-5 h-5' : 'w-4 h-4'}"
|
|
></div>
|
|
{:else}
|
|
<svg
|
|
class={size === "lg" ? "w-5 h-5" : "w-4 h-4"}
|
|
fill="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6a7 7 0 1 1 7 7c-1.93
|
|
0-3.68-.79-4.94-2.06l-1.42 1.42A8.95 8.95 0 0 0 13 21a9 9 0 0 0
|
|
0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
{busy ? "Clearing…" : label}
|
|
</button>
|