diff --git a/front/src/app/(app)/series/[slug].tsx b/front/src/app/(app)/series/[slug].tsx
new file mode 100644
index 00000000..9e113e33
--- /dev/null
+++ b/front/src/app/(app)/series/[slug].tsx
@@ -0,0 +1,3 @@
+import { SerieDetails } from "~/ui/details";
+
+export default SerieDetails;
diff --git a/front/src/components/entries/entry-list.tsx b/front/src/components/entries/entry-line.tsx
similarity index 96%
rename from front/src/components/entries/entry-list.tsx
rename to front/src/components/entries/entry-line.tsx
index e0d350cc..9a5cbd05 100644
--- a/front/src/components/entries/entry-list.tsx
+++ b/front/src/components/entries/entry-line.tsx
@@ -6,7 +6,7 @@ import { Platform, type PressableProps, View } from "react-native";
import { percent, type Stylable, useYoshiki } from "yoshiki/native";
import { EntryContext } from "~/components/items/context-menus";
import { ItemProgress } from "~/components/items/item-grid";
-import type { KImage, WatchStatusV } from "~/models";
+import type { KImage } from "~/models";
import {
focusReset,
H6,
@@ -34,7 +34,6 @@ export const EntryLine = ({
airDate,
runtime,
watchedPercent,
- watchedStatus,
href,
...props
}: {
@@ -48,7 +47,6 @@ export const EntryLine = ({
airDate: Date | null;
runtime: number | null;
watchedPercent: number | null;
- watchedStatus: WatchStatusV | null;
href: string;
} & PressableProps) => {
const [moreOpened, setMoreOpened] = useState(false);
@@ -92,7 +90,7 @@ export const EntryLine = ({
}}
{...(css({ flexShrink: 0, m: ts(1), borderRadius: 6 }) as any)}
>
- {(watchedPercent || watchedStatus === "completed") && (
+ {watchedPercent && (
)}
@@ -124,7 +122,6 @@ export const EntryLine = ({
setMoreOpened(v)}
{...css([
diff --git a/front/src/components/entries/index.ts b/front/src/components/entries/index.ts
index 02ceb3cb..9c3073e7 100644
--- a/front/src/components/entries/index.ts
+++ b/front/src/components/entries/index.ts
@@ -1,16 +1,17 @@
-export * from "./entry-box";
-export * from "./entry-list";
+import type { Entry } from "~/models";
-export const episodeDisplayNumber = (episode: {
- seasonNumber?: number | null;
- episodeNumber?: number | null;
- absoluteNumber?: number | null;
-}) => {
- if (
- typeof episode.seasonNumber === "number" &&
- typeof episode.episodeNumber === "number"
- )
- return `S${episode.seasonNumber}:E${episode.episodeNumber}`;
- if (episode.absoluteNumber) return episode.absoluteNumber.toString();
- return "??";
+export * from "./entry-box";
+export * from "./entry-line";
+
+export const entryDisplayNumber = (entry: Entry) => {
+ switch (entry.kind) {
+ case "episode":
+ return `S${entry.seasonNumber}:E${entry.episodeNumber}`;
+ case "special":
+ return `SP${entry.number}`
+ case "movie":
+ return "";
+ default:
+ return "??";
+ }
};
diff --git a/front/src/components/items/context-menus.tsx b/front/src/components/items/context-menus.tsx
index 0fc68802..b3d6875b 100644
--- a/front/src/components/items/context-menus.tsx
+++ b/front/src/components/items/context-menus.tsx
@@ -15,14 +15,54 @@ import { watchListIcon } from "./watchlist-info";
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
export const EntryContext = ({
- kind = "entry",
slug,
serieSlug,
+ ...props
+}: {
+ serieSlug: string | null;
+ slug: string;
+} & Partial>>) => {
+ // const downloader = useDownloader();
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export const ItemContext = ({
+ kind,
+ slug,
status,
...props
}: {
- kind?: "serie" | "movie" | "entry";
- serieSlug?: string | null;
+ kind: "movie" | "serie";
slug: string;
status: WatchStatusV | null;
} & Partial>>) => {
@@ -32,10 +72,7 @@ export const EntryContext = ({
const { t } = useTranslation();
const mutation = useMutation({
- path:
- kind === "entry"
- ? ["serie", serieSlug!, "entries", slug]
- : [kind, slug, "watchStatus"],
+ path: [kind, slug, "watchStatus"],
compute: (newStatus: WatchStatusV | null) => ({
method: newStatus ? "POST" : "DELETE",
params: newStatus ? { status: newStatus } : undefined,
@@ -55,18 +92,8 @@ export const EntryContext = ({
Trigger={IconButton}
icon={MoreVert}
{...tooltip(t("misc.more"))}
- {...(css(
- [Platform.OS !== "web" && { display: "none" }],
- props,
- ) as any)}
+ {...(css([Platform.OS !== "web" && { display: "none" }], props) as any)}
>
- {serieSlug && (
-
- )}
)}
- {kind !== "serie" && (
+ {kind === "movie" && (
<>
{/*
>
)}
@@ -117,24 +144,3 @@ export const EntryContext = ({
>
);
};
-
-export const ItemContext = ({
- kind,
- slug,
- status,
- ...props
-}: {
- kind: "movie" | "serie";
- slug: string;
- status: WatchStatusV | null;
-} & Partial>>) => {
- return (
-
- );
-};
diff --git a/front/src/models/entry.ts b/front/src/models/entry.ts
index e97462f0..d11fad2a 100644
--- a/front/src/models/entry.ts
+++ b/front/src/models/entry.ts
@@ -74,9 +74,11 @@ export const Special = Base.extend({
});
export type Special = z.infer;
-export const Entry = z.discriminatedUnion("kind", [
- Episode,
- MovieEntry,
- Special,
-]);
+export const Entry = z
+ .discriminatedUnion("kind", [Episode, MovieEntry, Special])
+ .transform((x) => ({
+ ...x,
+ // TODO: don't just pick the first video, be smart about it
+ href: x.videos.length ? `/watch/${x.videos[0].slug}` : null,
+ }));
export type Entry = z.infer;
diff --git a/front/src/primitives/utils/index.tsx b/front/src/primitives/utils/index.tsx
index c11b2f9c..9dece0d5 100644
--- a/front/src/primitives/utils/index.tsx
+++ b/front/src/primitives/utils/index.tsx
@@ -2,6 +2,5 @@ export * from "./breakpoint";
export * from "./capitalize";
export * from "./head";
export * from "./nojs";
-export * from "./page-style";
export * from "./spacing";
export * from "./touchonly";
diff --git a/front/src/primitives/utils/page-style.tsx b/front/src/primitives/utils/page-style.tsx
deleted file mode 100644
index fc658785..00000000
--- a/front/src/primitives/utils/page-style.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-
-export const usePageStyle = () => {
- const insets = useSafeAreaInsets();
- return { paddingBottom: insets.bottom } as const;
-};
diff --git a/front/src/primitives/utils/page-style.web.tsx b/front/src/primitives/utils/page-style.web.tsx
deleted file mode 100644
index ced291ba..00000000
--- a/front/src/primitives/utils/page-style.web.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export const usePageStyle = () => {
- return {} as const;
-};
diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx
index 3ebca5a7..01a7a234 100644
--- a/front/src/query/fetch-infinite.tsx
+++ b/front/src/query/fetch-infinite.tsx
@@ -12,7 +12,7 @@ export type Layout = {
layout: "grid" | "horizontal" | "vertical";
};
-export const InfiniteFetch = ({
+export const InfiniteFetch = ({
query,
placeholderCount = 2,
incremental = false,
@@ -22,11 +22,7 @@ export const InfiniteFetch = ({
Empty,
divider,
Header,
- headerProps,
- getItemType,
- getItemSize,
fetchMore = true,
- nested = false,
...props
}: {
query: QueryIdentifier;
@@ -39,11 +35,7 @@ export const InfiniteFetch = ({
incremental?: boolean;
divider?: true | ComponentType;
Header?: ComponentType | ReactElement;
- headerProps?: Props;
- getItemType?: (item: Data | null, index: number) => Kind;
- getItemSize?: (kind: Kind) => number;
fetchMore?: boolean;
- nested?: boolean;
}): JSX.Element | null => {
const { numColumns, size, gap } = useBreakpointMap(layout);
const [setOffline, clearOffline] = useSetError("offline");
diff --git a/front/src/ui/details/season.tsx b/front/src/ui/details/season.tsx
index de44d569..f9a5fbd7 100644
--- a/front/src/ui/details/season.tsx
+++ b/front/src/ui/details/season.tsx
@@ -1,9 +1,10 @@
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
-import type { ComponentType } from "react";
+import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { rem, useYoshiki } from "yoshiki/native";
-import { type Episode, type Season, useInfiniteFetch } from "~/models";
+import { EntryLine, entryDisplayNumber } from "~/components/entries";
+import { Entry, Season } from "~/models";
import {
H2,
HR,
@@ -13,22 +14,21 @@ import {
Skeleton,
tooltip,
ts,
- usePageStyle,
} from "~/primitives";
-import type { QueryIdentifier } from "~/query";
+import { type QueryIdentifier, useInfiniteFetch } from "~/query";
import { InfiniteFetch } from "~/query/fetch-infinite";
-import { EpisodeLine, episodeDisplayNumber } from "./episode";
-
-type SeasonProcessed = Season & { href: string };
+import { EmptyView } from "~/ui/errors";
export const SeasonHeader = ({
+ serieSlug,
seasonNumber,
name,
seasons,
}: {
+ serieSlug: string;
seasonNumber: number;
name: string | null;
- seasons?: SeasonProcessed[];
+ seasons: Season[];
}) => {
const { css } = useYoshiki();
const { t } = useTranslation();
@@ -62,17 +62,15 @@ export const SeasonHeader = ({
icon={MenuIcon}
{...tooltip(t("show.jumpToSeason"))}
>
- {seasons
- ?.filter((x) => x.episodesCount > 0)
- .map((x) => (
-
- ))}
+ {seasons.map((x) => (
+
+ ))}
@@ -115,33 +113,22 @@ SeasonHeader.Loader = () => {
SeasonHeader.query = (slug: string): QueryIdentifier => ({
parser: Season,
- path: ["series", slug, "seasons"],
+ path: ["api", "series", slug, "seasons"],
params: {
- // Fetch all seasons at one, there won't be hundred of thems anyways.
+ // Fetch all seasons at one, there won't be hundred of them anyways.
limit: 0,
},
- infinite: {
- value: true,
- map: (seasons) =>
- seasons.map((x) => ({
- ...x,
- href: `/show/${slug}?season=${x.seasonNumber}`,
- })),
- },
+ infinite: true,
});
-export const EpisodeList = ({
+export const EntryList = ({
slug,
season,
- Header,
- headerProps,
+ ...props
}: {
slug: string;
season: string | number;
- Header: ComponentType;
- headerProps: Props;
-}) => {
- const pageStyle = usePageStyle();
+} & Partial>) => {
const { t } = useTranslation();
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
@@ -149,40 +136,35 @@ export const EpisodeList = ({
return (
}
divider
- Header={Header}
- headerProps={headerProps}
- getItemType={(item) =>
- !item || item.firstOfSeason ? "withHeader" : "normal"
- }
- contentContainerStyle={pageStyle}
+ // getItemType={(item) =>
+ // item.kind === "episode" && item.episodeNumber === 1? "withHeader" : "normal"
+ // }
placeholderCount={5}
Render={({ item }) => {
- const sea = item?.firstOfSeason
- ? seasons?.find((x) => x.seasonNumber === item.seasonNumber)
- : null;
+ const sea =
+ item.kind === "episode" && item.episodeNumber === 1
+ ? seasons?.find((x) => x.seasonNumber === item.seasonNumber)
+ : null;
return (
<>
- {item.firstOfSeason &&
- (sea ? (
-
- ) : (
-
- ))}
-
+ )}
+
>
);
@@ -190,32 +172,22 @@ export const EpisodeList = ({
Loader={({ index }) => (
<>
{index === 0 && }
-
+
>
)}
+ {...props}
/>
);
};
-EpisodeList.query = (
+EntryList.query = (
slug: string,
season: string | number,
-): QueryIdentifier => ({
- parser: EpisodeP,
- path: ["show", slug, "episode"],
+): QueryIdentifier => ({
+ parser: Entry,
+ path: ["api", "series", slug, "entries"],
params: {
filter: season ? `seasonNumber gte ${season}` : undefined,
- fields: ["watchStatus"],
- },
- infinite: {
- value: true,
- map: (episodes) => {
- let currentSeason: number | null = null;
- return episodes.map((x) => {
- if (x.seasonNumber === currentSeason) return x;
- currentSeason = x.seasonNumber;
- return { ...x, firstOfSeason: true };
- });
- },
},
+ infinite: true,
});
diff --git a/front/src/ui/details/serie.tsx b/front/src/ui/details/serie.tsx
index 4536d83e..6d3fa55d 100644
--- a/front/src/ui/details/serie.tsx
+++ b/front/src/ui/details/serie.tsx
@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import Svg, { Path, type SvgProps } from "react-native-svg";
import { percent, useYoshiki } from "yoshiki/native";
+import { EntryLine, entryDisplayNumber } from "~/components/entries";
import { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives";
import { useQueryState } from "~/utils";
-import { EpisodeLine, episodeDisplayNumber } from "./episode";
import { Header } from "./header";
-import { EpisodeList } from "./season";
+import { EntryList } from "./season";
export const SvgWave = (props: SvgProps) => {
const { css } = useYoshiki();
@@ -31,7 +31,6 @@ export const SvgWave = (props: SvgProps) => {
export const ShowWatchStatusCard = ({
watchedPercent,
- status,
nextEpisode,
}: ShowWatchStatus) => {
const { t } = useTranslation();
@@ -60,12 +59,11 @@ export const ShowWatchStatusCard = ({
])}
>
{t("show.nextUp")}
- setFocus(true)}
onHoverOut={() => setFocus(false)}
onFocus={() => setFocus(true)}
@@ -77,8 +75,9 @@ export const ShowWatchStatusCard = ({
);
};
-const ShowHeader = ({ children, slug, ...props }: any) => {
+const SerieHeader = ({ children, ...props }: any) => {
const { css, theme } = useYoshiki();
+ const [slug] = useQueryState("slug", undefined!);
return (
{
);
};
-export const ShowDetails = () => {
+export const SerieDetails = () => {
const { css, theme } = useYoshiki();
const [slug] = useQueryState("slug", undefined!);
+ const [season] = useQueryState("season", undefined!);
return (
-
+
);
};