Add video mapper component

This commit is contained in:
Zoe Roux 2026-02-23 17:03:34 +01:00
parent 1965b5294f
commit b84d712037
No known key found for this signature in database
8 changed files with 98 additions and 45 deletions

View File

@ -28,17 +28,6 @@ export default function Layout() {
},
headerTintColor: color as string,
}}
>
<Stack.Screen
name="info/[slug]"
options={{
presentation: "transparentModal",
headerShown: false,
contentStyle: {
backgroundColor: "transparent",
},
}}
/>
</Stack>
/>
);
}

View File

@ -0,0 +1,3 @@
import { VideosModal } from "~/ui/admin";
export default VideosModal;

View File

@ -3,7 +3,7 @@ import type { Entry } from "~/models";
export * from "./entry-box";
export * from "./entry-line";
export const entryDisplayNumber = (entry: Entry) => {
export const entryDisplayNumber = (entry: Partial<Entry>) => {
switch (entry.kind) {
case "episode":
return `S${entry.seasonNumber}:E${entry.episodeNumber}`;

View File

@ -1,5 +1,5 @@
import { z } from "zod/v4";
import { Entry } from "./entry";
import { Entry, Episode, MovieEntry, Special } from "./entry";
import { Extra } from "./extra";
import { Show } from "./show";
import { zdate } from "./utils/utils";
@ -37,14 +37,21 @@ export const Video = z.object({
});
export const FullVideo = Video.extend({
slugs: z.array(z.string()),
progress: z.object({
percent: z.int().min(0).max(100),
time: z.int().min(0),
playedDate: zdate().nullable(),
videoId: z.string().nullable(),
}),
entries: z.array(Entry),
entries: z.array(
z.discriminatedUnion("kind", [
Episode.omit({ progress: true, videos: true }),
MovieEntry.omit({ progress: true, videos: true }),
Special.omit({ progress: true, videos: true }),
]),
),
progress: z.optional(
z.object({
percent: z.int().min(0).max(100),
time: z.int().min(0),
playedDate: zdate().nullable(),
videoId: z.string().nullable(),
}),
),
previous: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
next: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
show: Show.optional(),

View File

@ -1,5 +1,5 @@
import Close from "@material-symbols/svg-400/rounded/close.svg";
import { useRouter } from "expo-router";
import { Stack, useRouter } from "expo-router";
import type { ReactNode } from "react";
import { Pressable, ScrollView, View } from "react-native";
import { cn } from "~/utils";
@ -9,37 +9,50 @@ import { Heading } from "./text";
export const Modal = ({
title,
children,
scroll = true,
}: {
title: string;
children: ReactNode;
scroll?: boolean;
}) => {
const router = useRouter();
return (
<Pressable
className="absolute inset-0 cursor-default! items-center justify-center bg-black/60 max-md:px-4"
onPress={() => {
if (router.canGoBack()) router.back();
}}
>
<>
<Stack.Screen
options={{
presentation: "transparentModal",
headerShown: false,
contentStyle: {
backgroundColor: "transparent",
},
}}
/>
<Pressable
className={cn(
"w-full max-w-3xl rounded-md bg-background p-6",
"max-h-[90vh] cursor-default! overflow-hidden",
)}
onPress={(e) => e.stopPropagation()}
className="absolute inset-0 cursor-default! items-center justify-center bg-black/60 max-md:px-4"
onPress={() => {
if (router.canGoBack()) router.back();
}}
>
<View className="mb-4 flex-row items-center justify-between">
<Heading>{title}</Heading>
<IconButton
icon={Close}
onPress={() => {
if (router.canGoBack()) router.back();
}}
/>
</View>
<ScrollView>{children}</ScrollView>
<Pressable
className={cn(
"w-full max-w-3xl rounded-md bg-background",
"max-h-[90vh] cursor-default overflow-hidden *:p-6",
)}
onPress={(e) => e.preventDefault()}
>
<View className="mb-4 flex-row items-center justify-between">
<Heading>{title}</Heading>
<IconButton
icon={Close}
onPress={() => {
if (router.canGoBack()) router.back();
}}
/>
</View>
{scroll ? <ScrollView>{children}</ScrollView> : children}
</Pressable>
</Pressable>
</Pressable>
</>
);
};

View File

@ -0,0 +1 @@
export * from "./videos-modal";

View File

@ -0,0 +1,40 @@
import { View } from "react-native";
import { entryDisplayNumber } from "~/components/entries";
import { FullVideo } from "~/models";
import { Modal, P, Select, Skeleton } from "~/primitives";
import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { useQueryState } from "~/utils";
export const VideosModal = () => {
const [slug] = useQueryState<string>("slug", undefined!);
return (
<Modal title="toto" scroll={false}>
<InfiniteFetch
query={VideosModal.query(slug)}
layout={{ layout: "vertical", gap: 8, numColumns: 1, size: 48 }}
Render={({ item }) => (
<View className="h-12 flex-row items-center justify-between hover:bg-card">
<P>{item.path}</P>
<Select
label={"toto"}
value={1}
values={[1, 2, 3]}
getLabel={() =>
item.entries.map((x) => entryDisplayNumber(x)).join(", ")
}
onValueChange={(x) => {}}
/>
</View>
)}
Loader={() => <Skeleton />}
/>
</Modal>
);
};
VideosModal.query = (slug: string): QueryIdentifier<FullVideo> => ({
parser: FullVideo,
path: ["api", "series", slug, "videos"],
infinite: true,
});