diff --git a/front/src/app/(app)/_layout.tsx b/front/src/app/(app)/_layout.tsx
index eeafe777..e64491bd 100644
--- a/front/src/app/(app)/_layout.tsx
+++ b/front/src/app/(app)/_layout.tsx
@@ -28,17 +28,6 @@ export default function Layout() {
},
headerTintColor: color as string,
}}
- >
-
-
+ />
);
}
diff --git a/front/src/app/(app)/series/[slug].tsx b/front/src/app/(app)/series/[slug]/index.tsx
similarity index 100%
rename from front/src/app/(app)/series/[slug].tsx
rename to front/src/app/(app)/series/[slug]/index.tsx
diff --git a/front/src/app/(app)/series/[slug]/videos.tsx b/front/src/app/(app)/series/[slug]/videos.tsx
new file mode 100644
index 00000000..757712ea
--- /dev/null
+++ b/front/src/app/(app)/series/[slug]/videos.tsx
@@ -0,0 +1,3 @@
+import { VideosModal } from "~/ui/admin";
+
+export default VideosModal;
diff --git a/front/src/components/entries/index.ts b/front/src/components/entries/index.ts
index aecb4df4..f472eb6b 100644
--- a/front/src/components/entries/index.ts
+++ b/front/src/components/entries/index.ts
@@ -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) => {
switch (entry.kind) {
case "episode":
return `S${entry.seasonNumber}:E${entry.episodeNumber}`;
diff --git a/front/src/models/video.ts b/front/src/models/video.ts
index 50b81263..80fa5f3f 100644
--- a/front/src/models/video.ts
+++ b/front/src/models/video.ts
@@ -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(),
diff --git a/front/src/primitives/modal.tsx b/front/src/primitives/modal.tsx
index f7a3d912..0d8f7171 100644
--- a/front/src/primitives/modal.tsx
+++ b/front/src/primitives/modal.tsx
@@ -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 (
- {
- if (router.canGoBack()) router.back();
- }}
- >
+ <>
+
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();
+ }}
>
-
- {title}
- {
- if (router.canGoBack()) router.back();
- }}
- />
-
- {children}
+ e.preventDefault()}
+ >
+
+ {title}
+ {
+ if (router.canGoBack()) router.back();
+ }}
+ />
+
+ {scroll ? {children} : children}
+
-
+ >
);
};
diff --git a/front/src/ui/admin/index.tsx b/front/src/ui/admin/index.tsx
new file mode 100644
index 00000000..d75d2289
--- /dev/null
+++ b/front/src/ui/admin/index.tsx
@@ -0,0 +1 @@
+export * from "./videos-modal";
diff --git a/front/src/ui/admin/videos-modal.tsx b/front/src/ui/admin/videos-modal.tsx
new file mode 100644
index 00000000..7d1e5b3c
--- /dev/null
+++ b/front/src/ui/admin/videos-modal.tsx
@@ -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("slug", undefined!);
+
+ return (
+
+ (
+
+ {item.path}
+
+ )}
+ Loader={() => }
+ />
+
+ );
+};
+
+VideosModal.query = (slug: string): QueryIdentifier => ({
+ parser: FullVideo,
+ path: ["api", "series", slug, "videos"],
+ infinite: true,
+});