diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts
index c05a96e8..1d13dc70 100644
--- a/api/src/controllers/shows/logic.ts
+++ b/api/src/controllers/shows/logic.ts
@@ -41,6 +41,8 @@ import {
getEntryTransQ,
mapProgress,
} from "../entries";
+import { Collection } from "~/models/collections";
+import { alias } from "drizzle-orm/pg-core";
export const watchStatusQ = db
.select({
@@ -124,6 +126,42 @@ export const showRelations = {
.where(eq(showTranslations.pk, shows.pk))
.as("translations");
},
+ collection: ({
+ languages,
+ preferOriginal,
+ }: {
+ languages: string[];
+ preferOriginal?: boolean;
+ }) => {
+ const collections = alias(shows, "collections");
+ const colTrans = alias(showTranslations, "col_trans");
+ const transQ = db
+ .selectDistinctOn([colTrans.pk])
+ .from(colTrans)
+ .orderBy(
+ colTrans.pk,
+ sql`array_position(${sqlarr(languages)}, ${colTrans.language})`,
+ )
+ .as("t");
+
+ return db
+ .select({
+ json: jsonbBuildObject({
+ ...getColumns(collections),
+ ...getColumns(transQ),
+ ...(preferOriginal && {
+ poster: sql`coalesce(nullif(${collections.original}->'poster', 'null'::jsonb), ${transQ.poster})`,
+ thumbnail: sql`coalesce(nullif(${collections.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`,
+ banner: sql`coalesce(nullif(${collections.original}->'banner', 'null'::jsonb), ${transQ.banner})`,
+ logo: sql`coalesce(nullif(${collections.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
+ }),
+ }),
+ })
+ .from(collections)
+ .innerJoin(transQ, eq(collections.pk, transQ.pk))
+ .where(eq(collections.pk, shows.collectionPk))
+ .as("collection");
+ },
studios: ({ languages }: { languages: string[] }) => {
const studioTransQ = db
.selectDistinctOn([studioTranslations.pk])
@@ -304,7 +342,10 @@ export async function getShows({
watchStatus: getColumns(watchStatusQ),
- ...buildRelations(relations, showRelations, { languages }),
+ ...buildRelations(relations, showRelations, {
+ languages,
+ preferOriginal,
+ }),
})
.from(shows)
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts
index 79bf33bb..7c23a926 100644
--- a/api/src/controllers/shows/movies.ts
+++ b/api/src/controllers/shows/movies.ts
@@ -77,10 +77,13 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
preferOriginal: t.Optional(
t.Boolean({ description: desc.preferOriginal }),
),
- with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), {
- default: [],
- description: "Include related resources in the response.",
- }),
+ with: t.Array(
+ t.UnionEnum(["translations", "collection", "studios", "videos"]),
+ {
+ default: [],
+ description: "Include related resources in the response.",
+ },
+ ),
}),
headers: t.Object({
"accept-language": AcceptLanguage(),
@@ -119,10 +122,13 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
preferOriginal: t.Optional(
t.Boolean({ description: desc.preferOriginal }),
),
- with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), {
- default: [],
- description: "Include related resources in the response.",
- }),
+ with: t.Array(
+ t.UnionEnum(["translations", "collection", "studios", "videos"]),
+ {
+ default: [],
+ description: "Include related resources in the response.",
+ },
+ ),
}),
response: {
302: t.Void({
diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts
index 73769481..3672d35e 100644
--- a/api/src/controllers/shows/series.ts
+++ b/api/src/controllers/shows/series.ts
@@ -78,7 +78,13 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
t.Boolean({ description: desc.preferOriginal }),
),
with: t.Array(
- t.UnionEnum(["translations", "studios", "firstEntry", "nextEntry"]),
+ t.UnionEnum([
+ "translations",
+ "collection",
+ "studios",
+ "firstEntry",
+ "nextEntry",
+ ]),
{
default: [],
description: "Include related resources in the response.",
@@ -123,7 +129,13 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
t.Boolean({ description: desc.preferOriginal }),
),
with: t.Array(
- t.UnionEnum(["translations", "studios", "firstEntry", "nextEntry"]),
+ t.UnionEnum([
+ "translations",
+ "collection",
+ "studios",
+ "firstEntry",
+ "nextEntry",
+ ]),
{
default: [],
description: "Include related resources in the response.",
diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts
index e6fb514c..ace47e2a 100644
--- a/api/src/models/movie.ts
+++ b/api/src/models/movie.ts
@@ -1,6 +1,6 @@
import { t } from "elysia";
import type { Prettify } from "~/utils";
-import { SeedCollection } from "./collections";
+import { Collection, SeedCollection } from "./collections";
import { bubble, bubbleImages, registerExamples } from "./examples";
import { SeedStaff } from "./staff";
import { SeedStudio, Studio } from "./studio";
@@ -70,6 +70,7 @@ export const FullMovie = t.Intersect([
t.Object({
translations: t.Optional(TranslationRecord(MovieTranslation)),
videos: t.Optional(t.Array(EmbeddedVideo)),
+ collection: t.Optional(t.Nullable(Collection)),
studios: t.Optional(t.Array(Studio)),
}),
]);
diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts
index 640d7575..9feb8e41 100644
--- a/api/src/models/serie.ts
+++ b/api/src/models/serie.ts
@@ -1,6 +1,6 @@
import { t } from "elysia";
import type { Prettify } from "~/utils";
-import { SeedCollection } from "./collections";
+import { Collection, SeedCollection } from "./collections";
import { Entry, SeedEntry, SeedExtra } from "./entry";
import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
import { SeedSeason } from "./season";
@@ -84,6 +84,7 @@ export const FullSerie = t.Intersect([
Serie,
t.Object({
translations: t.Optional(TranslationRecord(SerieTranslation)),
+ collection: t.Optional(t.Nullable(Collection)),
studios: t.Optional(t.Array(Studio)),
firstEntry: t.Optional(t.Nullable(Entry)),
nextEntry: t.Optional(t.Nullable(Entry)),
diff --git a/front/public/translations/en.json b/front/public/translations/en.json
index 84fc33c2..7aaea08b 100644
--- a/front/public/translations/en.json
+++ b/front/public/translations/en.json
@@ -25,6 +25,7 @@
"episode-none": "There is no episodes in this season",
"episodeNoMetadata": "No metadata available",
"tags": "Tags",
+ "tags-none": "No tags available",
"links": "Links",
"jumpToSeason": "Jump to season",
"partOf": "Part of the",
diff --git a/front/src/components/items/index.ts b/front/src/components/items/index.ts
index 30d220e6..69abea5a 100644
--- a/front/src/components/items/index.ts
+++ b/front/src/components/items/index.ts
@@ -13,7 +13,7 @@ export const itemMap = (
subtitle: item.kind !== "collection" ? getDisplayDate(item) : null,
href: item.href,
poster: item.poster,
- thumbnail: item.thumbnail,
+ banner: item.banner ?? item.thumbnail,
watchStatus:
item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
watchPercent:
diff --git a/front/src/components/items/item-list.tsx b/front/src/components/items/item-list.tsx
index fb8e6533..ce50162f 100644
--- a/front/src/components/items/item-list.tsx
+++ b/front/src/components/items/item-list.tsx
@@ -22,7 +22,7 @@ export const ItemList = ({
kind,
name,
subtitle,
- thumbnail,
+ banner,
poster,
watchStatus,
availableCount,
@@ -36,7 +36,7 @@ export const ItemList = ({
name: string;
subtitle: string | null;
poster: KImage | null;
- thumbnail: KImage | null;
+ banner: KImage | null;
watchStatus: WatchStatusV | null;
availableCount?: number | null;
seenCount?: number | null;
@@ -56,7 +56,7 @@ export const ItemList = ({
{...props}
>
diff --git a/front/src/models/movie.ts b/front/src/models/movie.ts
index 8b00b1cc..a81ede6a 100644
--- a/front/src/models/movie.ts
+++ b/front/src/models/movie.ts
@@ -4,6 +4,7 @@ import { Genre } from "./utils/genre";
import { KImage } from "./utils/images";
import { Metadata } from "./utils/metadata";
import { zdate } from "./utils/utils";
+import { Collection } from "./collection";
export const Movie = z
.object({
@@ -64,6 +65,8 @@ export const Movie = z
percent: z.number().int().gte(0).lte(100),
})
.nullable(),
+
+ collection: Collection.optional().nullable(),
})
.transform((x) => ({
...x,
diff --git a/front/src/models/serie.ts b/front/src/models/serie.ts
index e8671382..abf030de 100644
--- a/front/src/models/serie.ts
+++ b/front/src/models/serie.ts
@@ -5,6 +5,7 @@ import { Genre } from "./utils/genre";
import { KImage } from "./utils/images";
import { Metadata } from "./utils/metadata";
import { zdate } from "./utils/utils";
+import { Collection } from "./collection";
export const Serie = z
.object({
@@ -62,6 +63,8 @@ export const Serie = z
seenCount: z.number().int().gte(0),
})
.nullable(),
+
+ collection: Collection.optional().nullable(),
})
.transform((x) => {
const entry = x.nextEntry ?? x.firstEntry;
diff --git a/front/src/primitives/container.tsx b/front/src/primitives/container.tsx
index e74e0f7d..b69cd521 100644
--- a/front/src/primitives/container.tsx
+++ b/front/src/primitives/container.tsx
@@ -1,24 +1,15 @@
-import type { ComponentType } from "react";
import { View, type ViewProps } from "react-native";
import { cn } from "~/utils";
-export const Container = ({
- className,
- as,
- ...props
-}: {
- className?: string;
- as?: ComponentType;
-} & AsProps) => {
- const As = as ?? View;
+export const Container = ({ className, ...props }: ViewProps) => {
return (
-
);
};
diff --git a/front/src/primitives/image-background.tsx b/front/src/primitives/image-background.tsx
index ee32340c..4577cea4 100644
--- a/front/src/primitives/image-background.tsx
+++ b/front/src/primitives/image-background.tsx
@@ -21,6 +21,7 @@ export const ImageBackground = ({
quality,
alt,
className,
+ children,
...props
}: {
src: KImage | null;
@@ -32,7 +33,11 @@ export const ImageBackground = ({
const { apiUrl, authToken } = useToken();
if (!src) {
- return ;
+ return (
+
+ {children}
+
+ );
}
const uri = `${apiUrl}${src[quality ?? "high"]}`;
@@ -54,7 +59,9 @@ export const ImageBackground = ({
className={cn("overflow-hidden bg-gray-300", className)}
imageStyle={{ width: "100%", height: "100%", margin: 0, padding: 0 }}
{...props}
- />
+ >
+ {children}
+
);
};
diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx
index cd4d42e9..e7f69032 100644
--- a/front/src/query/fetch-infinite.tsx
+++ b/front/src/query/fetch-infinite.tsx
@@ -27,6 +27,7 @@ export const InfiniteFetch = ({
Header,
fetchMore = true,
contentContainerStyle,
+ margin = true,
...props
}: {
query: QueryIdentifier;
@@ -47,6 +48,7 @@ export const InfiniteFetch = ({
contentContainerStyle?: ViewStyle;
onScroll?: LegendListProps["onScroll"];
scrollEventThrottle?: LegendListProps["scrollEventThrottle"];
+ margin?: boolean;
}): JSX.Element | null => {
const { numColumns, size, gap } = useBreakpointMap(layout);
const oldItems = useRef(undefined);
@@ -101,12 +103,15 @@ export const InfiniteFetch = ({
}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
- contentContainerStyle={{
- ...contentContainerStyle,
- gap,
- marginLeft: numColumns > 1 ? gap : 0,
- marginRight: numColumns > 1 ? gap : 0,
- }}
+ contentContainerStyle={[
+ {
+ ...contentContainerStyle,
+ gap,
+ },
+ margin
+ ? { marginLeft: gap, marginRight: gap }
+ : { marginLeft: 0, marginRight: 0 },
+ ]}
{...props}
/>
);
diff --git a/front/src/ui/details/collection.tsx b/front/src/ui/details/collection.tsx
index 2edb2647..c7c12218 100644
--- a/front/src/ui/details/collection.tsx
+++ b/front/src/ui/details/collection.tsx
@@ -1,30 +1,77 @@
import { useState } from "react";
-import Animated from "react-native-reanimated";
+import { View, type ViewProps } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { itemMap } from "~/components/items";
+import { ItemDetails } from "~/components/items/item-details";
+import { Show } from "~/models";
+import { Container } from "~/primitives";
+import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { useQueryState } from "~/utils";
import { HeaderBackground, useScrollNavbar } from "../navbar";
import { Header } from "./header";
+import { SvgWave } from "./serie";
+
+const CollectionHeader = ({
+ slug,
+ onImageLayout,
+}: {
+ slug: string;
+ onImageLayout?: ViewProps["onLayout"];
+}) => {
+ return (
+
+
+
+
+ );
+};
export const CollectionDetails = () => {
const [slug] = useQueryState("slug", undefined!);
const insets = useSafeAreaInsets();
const [imageHeight, setHeight] = useState(300);
- const { scrollHandler, headerProps } = useScrollNavbar({ imageHeight });
+ const { scrollHandler, headerProps } = useScrollNavbar({
+ imageHeight,
+ });
return (
- <>
+
- (
+
+ )}
+ Loader={() => (
+
+
+
+ )}
+ Header={() => (
+ setHeight(e.nativeEvent.layout.height)}
+ />
+ )}
onScroll={scrollHandler}
- scrollEventThrottle={16}
- contentContainerStyle={{ paddingBottom: insets.bottom }}
- >
- setHeight(e.nativeEvent.layout.height)}
- />
-
- >
+ contentContainerStyle={{
+ paddingBottom: insets.bottom,
+ }}
+ margin={false}
+ />
+
);
};
+
+CollectionDetails.query = (slug: string): QueryIdentifier => ({
+ parser: Show,
+ path: ["api", "collections", slug, "shows"],
+ infinite: true,
+});
diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx
index 7042fdf3..eb0be504 100644
--- a/front/src/ui/details/header.tsx
+++ b/front/src/ui/details/header.tsx
@@ -41,6 +41,7 @@ import {
import { useAccount } from "~/providers/account-context";
import { Fetch, type QueryIdentifier } from "~/query";
import { cn, displayRuntime, getDisplayDate } from "~/utils";
+import { PartOf } from "./part-of";
const ButtonList = ({
kind,
@@ -260,7 +261,7 @@ const Description = ({
description: string | null;
tags: string[];
genres: Genre[];
- studios: Studio[];
+ studios: Studio[] | null;
externalIds: Metadata;
}) => {
const { t } = useTranslation();
@@ -268,7 +269,7 @@ const Description = ({
return (
-
+
{description ?? t("show.noOverview")}
@@ -293,27 +294,33 @@ const Description = ({
{t("show.tags")}:
- {tags.map((tag) => (
-
- ))}
+ {tags.length ? (
+ tags.map((tag) => (
+
+ ))
+ ) : (
+ {t("show.tags-none")}
+ )}
-
-
{t("show.studios")}:
- {studios.map((x, i) => (
-
- {i !== 0 && ","}
-
- {x.name}
-
-
- ))}
-
+ {studios !== null && (
+
+
{t("show.studios")}:
+ {studios.map((x, i) => (
+
+ {i !== 0 && ","}
+
+ {x.name}
+
+
+ ))}
+
+ )}
{t("show.links")}:
{Object.entries(externalIds)
@@ -425,13 +432,20 @@ export const Header = ({
description={data.description}
tags={data.tags}
genres={data.genres}
- studios={data.kind !== "collection" ? data.studios! : []}
+ studios={data.kind !== "collection" ? data.studios! : null}
externalIds={data.externalId}
/>
- {/* {type === "show" && ( */}
- {/* */}
- {/* )} */}
+ {data.kind !== "collection" && data.collection && (
+
+
+
+ )}
)}
Loader={() => (
@@ -459,7 +473,7 @@ Header.query = (
path: ["api", `${kind}s`, slug],
params: {
with: [
- ...(kind !== "collection" ? ["studios"] : []),
+ ...(kind !== "collection" ? ["collection", "studios"] : []),
...(kind === "serie" ? ["firstEntry", "nextEntry"] : []),
],
},
diff --git a/front/src/ui/details/part-of.tsx b/front/src/ui/details/part-of.tsx
index 864ab92e..3b1e45aa 100644
--- a/front/src/ui/details/part-of.tsx
+++ b/front/src/ui/details/part-of.tsx
@@ -1,110 +1,47 @@
-import {
- type Collection,
- CollectionP,
- type KyooImage,
- type QueryIdentifier,
- useInfiniteFetch,
-} from "@kyoo/models";
-import {
- Container,
- focusReset,
- GradientImageBackground,
- H2,
- ImageBackground,
- Link,
- P,
- ts,
-} from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
-import { type Theme, useYoshiki } from "yoshiki/native";
+import { View } from "react-native";
+import type { KImage } from "~/models";
+import { H2, ImageBackground, Link, P } from "~/primitives";
+import { cn } from "~/utils";
export const PartOf = ({
name,
- overview,
- thumbnail,
+ description,
+ banner,
href,
+ className,
}: {
name: string;
- overview: string | null;
- thumbnail: KyooImage | null;
+ description: string | null;
+ banner: KImage | null;
href: string;
+ className?: string;
}) => {
- const { css, theme } = useYoshiki("part-of-collection");
const { t } = useTranslation();
return (
theme.background,
- fover: {
- self: { ...focusReset, borderColor: (theme: Theme) => theme.accent },
- title: { textDecorationLine: "underline" },
- },
- })}
+ className={cn(
+ "group flex-1 overflow-hidden rounded-xl ring-accent hover:ring-3 focus-visible:ring-3",
+ className,
+ )}
>
-
-
+
+
+
{t("show.partOf")} {name}
- {overview}
-
+
+ {description}
+
+
);
};
-
-export const DetailsCollections = ({
- type,
- slug,
-}: {
- type: "movie" | "show";
- slug: string;
-}) => {
- const { items } = useInfiniteFetch(DetailsCollections.query(type, slug));
- const { css } = useYoshiki();
-
- // Since most items dont have collections, not having a skeleton reduces layout shifts.
- if (!items) return null;
-
- return (
-
- {items.map((x) => (
-
- ))}
-
- );
-};
-
-DetailsCollections.query = (
- type: "movie" | "show",
- slug: string,
-): QueryIdentifier => ({
- parser: CollectionP,
- path: [type, slug, "collections"],
- params: {
- limit: 0,
- },
- infinite: true,
-});
diff --git a/front/src/ui/details/season.tsx b/front/src/ui/details/season.tsx
index 904e9828..87aa301b 100644
--- a/front/src/ui/details/season.tsx
+++ b/front/src/ui/details/season.tsx
@@ -123,34 +123,33 @@ export const EntryList = ({
.filter((x) => x !== null)
}
placeholderCount={5}
- Render={({ item }) =>
- item.kind === "season" ? (
-
- ) : (
-
- )
- }
- Loader={({ index }) =>
- index === 0 ? (
-
- ) : (
-
- )
- }
+ Render={({ item }) => (
+
+ {item.kind === "season" ? (
+
+ ) : (
+
+ )}
+
+ )}
+ Loader={({ index }) => (
+
+ {index === 0 ? : }
+
+ )}
+ margin={false}
{...props}
/>
);
diff --git a/front/src/ui/details/serie.tsx b/front/src/ui/details/serie.tsx
index a1ae2adb..c8d77196 100644
--- a/front/src/ui/details/serie.tsx
+++ b/front/src/ui/details/serie.tsx
@@ -74,7 +74,6 @@ const SerieHeader = ({
}}
Loader={NextUp.Loader}
/>
- {/* */}
{/* */}