feat(web): route season and episode release decks
This commit is contained in:
+59
-1
@@ -63,6 +63,7 @@ import {
|
|||||||
type ApiSeries,
|
type ApiSeries,
|
||||||
type EpisodeFile,
|
type EpisodeFile,
|
||||||
episodeTarget,
|
episodeTarget,
|
||||||
|
fetchEpisode,
|
||||||
fetchSeasons,
|
fetchSeasons,
|
||||||
fetchSeries,
|
fetchSeries,
|
||||||
fetchSeriesFiles,
|
fetchSeriesFiles,
|
||||||
@@ -305,7 +306,7 @@ function main() {
|
|||||||
// the surface underneath must be live before the detail view covers
|
// 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)
|
// it, so back and Esc land somewhere real (same as a movie deep link)
|
||||||
library.open();
|
library.open();
|
||||||
seriesDetail.open(
|
await seriesDetail.open(
|
||||||
route.seriesId,
|
route.seriesId,
|
||||||
must<HTMLElement>("#nav-library"),
|
must<HTMLElement>("#nav-library"),
|
||||||
must<HTMLElement>("#library"),
|
must<HTMLElement>("#library"),
|
||||||
@@ -315,6 +316,61 @@ function main() {
|
|||||||
);
|
);
|
||||||
break;
|
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) {
|
if (currentId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
navigate({ kind: "seasonReleases", seriesId: currentId, seasonNumber: season.number });
|
||||||
tvDeck.open({
|
tvDeck.open({
|
||||||
title: currentTitle,
|
title: currentTitle,
|
||||||
sub: `season ${PAD_TWO(season.number)} · packs`,
|
sub: `season ${PAD_TWO(season.number)} · packs`,
|
||||||
@@ -2470,6 +2527,7 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
|||||||
if (currentId === null) {
|
if (currentId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
navigate({ kind: "episodeReleases", episodeId: episode.id });
|
||||||
tvDeck.open({
|
tvDeck.open({
|
||||||
title: currentTitle,
|
title: currentTitle,
|
||||||
sub: `S${PAD_TWO(seasonNumber)}E${PAD_TWO(episode.number)} · ${episode.title}`,
|
sub: `S${PAD_TWO(seasonNumber)}E${PAD_TWO(episode.number)} · ${episode.title}`,
|
||||||
|
|||||||
+30
-1
@@ -9,7 +9,9 @@ export type Route =
|
|||||||
| { kind: "settings" }
|
| { kind: "settings" }
|
||||||
| { kind: "search"; query: string }
|
| { kind: "search"; query: string }
|
||||||
| { kind: "releases"; movieId: number }
|
| { 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 {
|
export function parseRoute(url: URL): Route {
|
||||||
const segments = url.pathname.split("/").filter(Boolean);
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
@@ -38,6 +40,29 @@ export function parseRoute(url: URL): Route {
|
|||||||
return { kind: "series", seriesId };
|
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" };
|
return { kind: "board" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +82,10 @@ export function routePath(route: Route): string {
|
|||||||
return `/movies/${route.movieId}/releases`;
|
return `/movies/${route.movieId}/releases`;
|
||||||
case "series":
|
case "series":
|
||||||
return `/series/${route.seriesId}`;
|
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 {
|
export interface ApiEpisode {
|
||||||
id: number;
|
id: number;
|
||||||
|
series_id: number;
|
||||||
season_id: number;
|
season_id: number;
|
||||||
|
season_number: number;
|
||||||
number: number;
|
number: number;
|
||||||
title: string;
|
title: string;
|
||||||
air_date: string | null;
|
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> {
|
export async function fetchSeasons(seriesId: number): Promise<SeasonsOutcome> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/series/${seriesId}/seasons`);
|
const response = await fetch(`/api/series/${seriesId}/seasons`);
|
||||||
|
|||||||
Reference in New Issue
Block a user