feat(web): route season and episode release decks
This commit is contained in:
+59
-1
@@ -63,6 +63,7 @@ import {
|
||||
type ApiSeries,
|
||||
type EpisodeFile,
|
||||
episodeTarget,
|
||||
fetchEpisode,
|
||||
fetchSeasons,
|
||||
fetchSeries,
|
||||
fetchSeriesFiles,
|
||||
@@ -305,7 +306,7 @@ function main() {
|
||||
// the surface underneath must be live before the detail view covers
|
||||
// it, so back and Esc land somewhere real (same as a movie deep link)
|
||||
library.open();
|
||||
seriesDetail.open(
|
||||
await seriesDetail.open(
|
||||
route.seriesId,
|
||||
must<HTMLElement>("#nav-library"),
|
||||
must<HTMLElement>("#library"),
|
||||
@@ -315,6 +316,61 @@ function main() {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "seasonReleases":
|
||||
case "episodeReleases": {
|
||||
// both deck routes live over the series detail view, exactly as a
|
||||
// click on a deck button does; resolve what that click would have
|
||||
let title: string;
|
||||
let sub: string;
|
||||
let seriesId: number;
|
||||
let target: TvTarget;
|
||||
if (route.kind === "seasonReleases") {
|
||||
seriesId = route.seriesId;
|
||||
target = seasonTarget(route.seriesId, route.seasonNumber);
|
||||
sub = `season ${PAD_TWO(route.seasonNumber)} · packs`;
|
||||
title = (await fetchSeries(route.seriesId))?.title ?? "";
|
||||
if (title === "") {
|
||||
navigate({ kind: "library" }, { replace: true });
|
||||
library.open();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const episode = await fetchEpisode(route.episodeId);
|
||||
if (!episode) {
|
||||
navigate({ kind: "library" }, { replace: true });
|
||||
library.open();
|
||||
return;
|
||||
}
|
||||
seriesId = episode.series_id;
|
||||
target = episodeTarget(episode.id);
|
||||
sub = `S${PAD_TWO(episode.season_number)}E${PAD_TWO(episode.number)} · ${episode.title}`;
|
||||
title = (await fetchSeries(seriesId))?.title ?? "";
|
||||
if (title === "") {
|
||||
navigate({ kind: "library" }, { replace: true });
|
||||
library.open();
|
||||
return;
|
||||
}
|
||||
}
|
||||
library.open();
|
||||
await seriesDetail.open(
|
||||
seriesId,
|
||||
must<HTMLElement>("#nav-library"),
|
||||
must<HTMLElement>("#library"),
|
||||
{ kind: "library" },
|
||||
);
|
||||
// no origin click to restore focus to on a deep link — the series
|
||||
// view's back control is the closest stand-in
|
||||
tvDeck.open({
|
||||
title,
|
||||
sub,
|
||||
seriesId,
|
||||
target,
|
||||
origin: must<HTMLButtonElement>("#series-back"),
|
||||
returnTo: must<HTMLElement>("#series"),
|
||||
parentRoute: { kind: "series", seriesId },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2327,6 +2383,7 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
if (currentId === null) {
|
||||
return;
|
||||
}
|
||||
navigate({ kind: "seasonReleases", seriesId: currentId, seasonNumber: season.number });
|
||||
tvDeck.open({
|
||||
title: currentTitle,
|
||||
sub: `season ${PAD_TWO(season.number)} · packs`,
|
||||
@@ -2470,6 +2527,7 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
if (currentId === null) {
|
||||
return;
|
||||
}
|
||||
navigate({ kind: "episodeReleases", episodeId: episode.id });
|
||||
tvDeck.open({
|
||||
title: currentTitle,
|
||||
sub: `S${PAD_TWO(seasonNumber)}E${PAD_TWO(episode.number)} · ${episode.title}`,
|
||||
|
||||
+30
-1
@@ -9,7 +9,9 @@ export type Route =
|
||||
| { kind: "settings" }
|
||||
| { kind: "search"; query: string }
|
||||
| { kind: "releases"; movieId: number }
|
||||
| { kind: "series"; seriesId: number };
|
||||
| { kind: "series"; seriesId: number }
|
||||
| { kind: "seasonReleases"; seriesId: number; seasonNumber: number }
|
||||
| { kind: "episodeReleases"; episodeId: number };
|
||||
|
||||
export function parseRoute(url: URL): Route {
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
@@ -38,6 +40,29 @@ export function parseRoute(url: URL): Route {
|
||||
return { kind: "series", seriesId };
|
||||
}
|
||||
}
|
||||
if (
|
||||
segments.length === 5 &&
|
||||
segments[0] === "series" &&
|
||||
segments[2] === "seasons" &&
|
||||
segments[4] === "releases"
|
||||
) {
|
||||
const seriesId = Number(segments[1]);
|
||||
const seasonNumber = Number(segments[3]);
|
||||
if (
|
||||
Number.isInteger(seriesId) &&
|
||||
seriesId > 0 &&
|
||||
Number.isInteger(seasonNumber) &&
|
||||
seasonNumber >= 0
|
||||
) {
|
||||
return { kind: "seasonReleases", seriesId, seasonNumber };
|
||||
}
|
||||
}
|
||||
if (segments.length === 3 && segments[0] === "episodes" && segments[2] === "releases") {
|
||||
const episodeId = Number(segments[1]);
|
||||
if (Number.isInteger(episodeId) && episodeId > 0) {
|
||||
return { kind: "episodeReleases", episodeId };
|
||||
}
|
||||
}
|
||||
return { kind: "board" };
|
||||
}
|
||||
|
||||
@@ -57,6 +82,10 @@ export function routePath(route: Route): string {
|
||||
return `/movies/${route.movieId}/releases`;
|
||||
case "series":
|
||||
return `/series/${route.seriesId}`;
|
||||
case "seasonReleases":
|
||||
return `/series/${route.seriesId}/seasons/${route.seasonNumber}/releases`;
|
||||
case "episodeReleases":
|
||||
return `/episodes/${route.episodeId}/releases`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ export interface ApiSeries {
|
||||
|
||||
export interface ApiEpisode {
|
||||
id: number;
|
||||
series_id: number;
|
||||
season_id: number;
|
||||
season_number: number;
|
||||
number: number;
|
||||
title: string;
|
||||
air_date: string | null;
|
||||
@@ -54,6 +56,16 @@ export async function fetchSeries(seriesId: number): Promise<ApiSeries | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/** One episode by row id — the `/episodes/{id}/releases` deep link's lookup. */
|
||||
export async function fetchEpisode(episodeId: number): Promise<ApiEpisode | null> {
|
||||
try {
|
||||
const response = await fetch(`/api/episodes/${episodeId}`);
|
||||
return response.ok ? ((await response.json()) as ApiEpisode) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSeasons(seriesId: number): Promise<SeasonsOutcome> {
|
||||
try {
|
||||
const response = await fetch(`/api/series/${seriesId}/seasons`);
|
||||
|
||||
Reference in New Issue
Block a user