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 ( + <> + + {serieSlug && ( + + )} + {/* downloader(type, slug)} */} + {/* /> */} + + + + ); +}; + +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 ( - + ); };