mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-16 22:49:15 -04:00
Split videos-modal code
This commit is contained in:
parent
8f82a03989
commit
58deb07b9c
@ -3,6 +3,7 @@ import {
|
||||
desc,
|
||||
eq,
|
||||
gt,
|
||||
inArray,
|
||||
isNotNull,
|
||||
lt,
|
||||
max,
|
||||
@ -11,8 +12,7 @@ import {
|
||||
or,
|
||||
type SQL,
|
||||
sql,
|
||||
inArray,
|
||||
WithSubquery,
|
||||
type WithSubquery,
|
||||
} from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import { Elysia, t } from "elysia";
|
||||
@ -33,7 +33,6 @@ import {
|
||||
jsonbBuildObject,
|
||||
jsonbObjectAgg,
|
||||
sqlarr,
|
||||
unnest,
|
||||
} from "~/db/utils";
|
||||
import type { Entry } from "~/models/entry";
|
||||
import { KError } from "~/models/error";
|
||||
@ -82,7 +81,7 @@ const videoSort = Sort(
|
||||
],
|
||||
},
|
||||
{
|
||||
default: ["entry", "path"],
|
||||
default: ["path"],
|
||||
tablePk: videos.pk,
|
||||
},
|
||||
);
|
||||
|
||||
15
biome.json
15
biome.json
@ -28,7 +28,8 @@
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off",
|
||||
"noTemplateCurlyInString": "off"
|
||||
"noTemplateCurlyInString": "off",
|
||||
"noDuplicateCustomProperties": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
@ -44,16 +45,8 @@
|
||||
"useSortedClasses": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"attributes": [
|
||||
"classList"
|
||||
],
|
||||
"functions": [
|
||||
"clsx",
|
||||
"cva",
|
||||
"cn",
|
||||
"tw",
|
||||
"tw.*"
|
||||
]
|
||||
"attributes": ["classList"],
|
||||
"functions": ["clsx", "cva", "cn", "tw", "tw.*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,13 +45,17 @@
|
||||
"season": "Season {{number}}",
|
||||
"multiVideos": "Multiples video files available",
|
||||
"videosCount": "{{number}} videos",
|
||||
"videos-map": "Edit video mappings",
|
||||
"videos-map-none": "NONE",
|
||||
"videos-map-add": "Add another video",
|
||||
"videos-map-delete": "Remove video from serie",
|
||||
"videos-map-validate": "Validate guess and add video to serie",
|
||||
"videos-map-no-guess": "This video was guessed to be part of this serie but we don't know which episode",
|
||||
"videos-map-related": "Added videos related to the title {{title}}"
|
||||
"videos-map": "Edit video mappings"
|
||||
},
|
||||
"videos-map": {
|
||||
"none": "NONE",
|
||||
"add": "Add another video",
|
||||
"delete": "Remove video from serie",
|
||||
"validate": "Validate guess and add video to serie",
|
||||
"no-guess": "This video was guessed to be part of this serie but we don't know which episode",
|
||||
"related": "Added videos related to the title {{title}}",
|
||||
"sort-path": "Path",
|
||||
"sort-entry": "Episode order"
|
||||
},
|
||||
"browse": {
|
||||
"mediatypekey": {
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import type { ComponentProps, ComponentType, Ref } from "react";
|
||||
import {
|
||||
type Falsy,
|
||||
type PressableProps,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { type Falsy, type PressableProps, View } from "react-native";
|
||||
import { cn } from "~/utils";
|
||||
import { Icon } from "./icons";
|
||||
import { PressableFeedback } from "./links";
|
||||
|
||||
@ -1,233 +0,0 @@
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import Question from "@material-symbols/svg-400/rounded/question_mark-fill.svg";
|
||||
import LibraryAdd from "@material-symbols/svg-400/rounded/library_add-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { entryDisplayNumber } from "~/components/entries";
|
||||
import { Entry, FullVideo, type Page } from "~/models";
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
IconButton,
|
||||
Modal,
|
||||
P,
|
||||
Skeleton,
|
||||
tooltip,
|
||||
} from "~/primitives";
|
||||
import {
|
||||
InfiniteFetch,
|
||||
type QueryIdentifier,
|
||||
useFetch,
|
||||
useMutation,
|
||||
} from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Header } from "../details/header";
|
||||
import { uniqBy } from "~/utils";
|
||||
|
||||
export const VideosModal = () => {
|
||||
const [slug] = useQueryState<string>("slug", undefined!);
|
||||
const { data } = useFetch(Header.query("serie", slug));
|
||||
const { t } = useTranslation();
|
||||
const [titles, setTitles] = useState<string[]>([]);
|
||||
|
||||
const { mutateAsync: editLinks } = useMutation({
|
||||
method: "PUT",
|
||||
path: ["api", "videos", "link"],
|
||||
compute: ({
|
||||
video,
|
||||
entries,
|
||||
guess = false,
|
||||
}: {
|
||||
video: string;
|
||||
entries: Omit<Entry, "href" | "progress" | "videos">[];
|
||||
guess?: boolean;
|
||||
}) => ({
|
||||
body: [
|
||||
{
|
||||
id: video,
|
||||
for: entries.map((x) =>
|
||||
guess && x.kind === "episode"
|
||||
? {
|
||||
serie: slug,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
season: x.seasonNumber,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
episode: x.episodeNumber,
|
||||
}
|
||||
: { slug: x.slug },
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
invalidate: ["api", "series", slug],
|
||||
optimisticKey: VideosModal.query(slug, null),
|
||||
optimistic: (params, prev?: { pages: Page<FullVideo>[] }) => ({
|
||||
...prev!,
|
||||
pages: prev!.pages.map((p) => ({
|
||||
...p,
|
||||
items: p!.items.map((x) => {
|
||||
if (x.id !== params.video) return x;
|
||||
return { ...x, entries: params.entries };
|
||||
}) as FullVideo[],
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal title={data?.name ?? t("misc.loading")} scroll={false}>
|
||||
{[...titles].map((title) => (
|
||||
<View
|
||||
key={title}
|
||||
className="m-2 flex-row items-center justify-between rounded bg-card px-6"
|
||||
>
|
||||
<P>{t("show.videos-map-related", { title })}</P>
|
||||
<IconButton
|
||||
icon={Close}
|
||||
onPress={() => {
|
||||
setTitles(titles.filter((x) => x !== title));
|
||||
}}
|
||||
{...tooltip(t("misc.cancel"))}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
<InfiniteFetch
|
||||
query={VideosModal.query(slug, titles)}
|
||||
layout={{ layout: "vertical", gap: 8, numColumns: 1, size: 48 }}
|
||||
Render={({ item }) => {
|
||||
const saved = item.entries.length;
|
||||
const guess = !saved
|
||||
? uniqBy(
|
||||
item.guess.episodes.map(
|
||||
(x) =>
|
||||
({
|
||||
kind: "episode",
|
||||
id: `s${x.season}-e${x.episode}`,
|
||||
seasonNumber: x.season,
|
||||
episodeNumber: x.episode,
|
||||
}) as Entry,
|
||||
),
|
||||
(x) => x.id,
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<View
|
||||
className="mx-6 h-12 flex-row items-center justify-between hover:bg-card"
|
||||
style={!saved && { opacity: 0.6 }}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
{saved ? (
|
||||
<IconButton
|
||||
icon={Close}
|
||||
onPress={async () => {
|
||||
if (!titles.includes(item.guess.title))
|
||||
setTitles([...titles, item.guess.title]);
|
||||
await editLinks({ video: item.id, entries: [] });
|
||||
}}
|
||||
{...tooltip(t("show.videos-map-delete"))}
|
||||
/>
|
||||
) : guess.length ? (
|
||||
<IconButton
|
||||
icon={Check}
|
||||
onPress={async () => {
|
||||
await editLinks({
|
||||
video: item.id,
|
||||
entries: guess,
|
||||
guess: true,
|
||||
});
|
||||
}}
|
||||
{...tooltip(t("show.videos-map-validate"))}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled
|
||||
icon={Question}
|
||||
{...tooltip(t("show.videos-map-no-guess"))}
|
||||
/>
|
||||
)}
|
||||
<P>{item.path}</P>
|
||||
</View>
|
||||
<ComboBox
|
||||
multiple
|
||||
label={t("show.videos-map-none")}
|
||||
searchPlaceholder={t("navbar.search")}
|
||||
values={saved ? item.entries : guess}
|
||||
query={(q) => ({
|
||||
parser: Entry,
|
||||
path: ["api", "series", slug, "entries"],
|
||||
params: {
|
||||
query: q,
|
||||
},
|
||||
infinite: true,
|
||||
})}
|
||||
getKey={
|
||||
saved
|
||||
? (x) => x.id
|
||||
: (x) =>
|
||||
x.kind === "episode"
|
||||
? `${x.seasonNumber}-${x.episodeNumber}`
|
||||
: x.id
|
||||
}
|
||||
getLabel={(x) => `${entryDisplayNumber(x)} - ${x.name}`}
|
||||
getSmallLabel={entryDisplayNumber}
|
||||
onValueChange={async (entries) => {
|
||||
if (!entries.length && !titles.includes(item.guess.title))
|
||||
setTitles([...titles, item.guess.title]);
|
||||
await editLinks({
|
||||
video: item.id,
|
||||
entries,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
Loader={() => <Skeleton />}
|
||||
Footer={
|
||||
<ComboBox
|
||||
Trigger={(props) => (
|
||||
<Button
|
||||
icon={LibraryAdd}
|
||||
text={t("show.videos-map-add")}
|
||||
className="m-6 mt-10"
|
||||
onPress={props.onPress ?? (props as any).onClick}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
searchPlaceholder={t("navbar.search")}
|
||||
value={null}
|
||||
query={(q) => ({
|
||||
parser: FullVideo,
|
||||
path: ["api", "videos"],
|
||||
params: {
|
||||
query: q,
|
||||
sort: "path",
|
||||
},
|
||||
infinite: true,
|
||||
})}
|
||||
getKey={(x) => x.id}
|
||||
getLabel={(x) => x.path}
|
||||
onValueChange={(x) => {
|
||||
if (x && !titles.includes(x.guess.title))
|
||||
setTitles([...titles, x.guess.title]);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
VideosModal.query = (
|
||||
slug: string,
|
||||
titles: string[] | null,
|
||||
): QueryIdentifier<FullVideo> => ({
|
||||
parser: FullVideo,
|
||||
path: ["api", "series", slug, "videos"],
|
||||
params: {
|
||||
sort: "path",
|
||||
titles: titles ?? undefined,
|
||||
},
|
||||
infinite: true,
|
||||
});
|
||||
92
front/src/ui/admin/videos-modal/headers.tsx
Normal file
92
front/src/ui/admin/videos-modal/headers.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import Path from "@material-symbols/svg-400/rounded/conversion_path-fill.svg";
|
||||
import LibraryAdd from "@material-symbols/svg-400/rounded/library_add-fill.svg";
|
||||
import Sort from "@material-symbols/svg-400/rounded/sort.svg";
|
||||
import Entry from "@material-symbols/svg-400/rounded/tv_next-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FullVideo } from "~/models";
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
Icon,
|
||||
Menu,
|
||||
P,
|
||||
PressableFeedback,
|
||||
tooltip,
|
||||
} from "~/primitives";
|
||||
|
||||
const sortModes = [
|
||||
["path", Path],
|
||||
["entry", Entry],
|
||||
] as const;
|
||||
|
||||
export const SortMenu = ({
|
||||
sort,
|
||||
setSort,
|
||||
}: {
|
||||
sort: "path" | "entry";
|
||||
setSort: (sort: "path" | "entry") => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Menu
|
||||
Trigger={(props) => (
|
||||
<PressableFeedback
|
||||
className="flex-row items-center"
|
||||
{...tooltip(t("browse.sortby-tt"))}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon={Sort} className="mx-1" />
|
||||
<P>{t(`videos-map.sort-${sort}`)}</P>
|
||||
</PressableFeedback>
|
||||
)}
|
||||
>
|
||||
{sortModes.map((x) => (
|
||||
<Menu.Item
|
||||
key={x[0]}
|
||||
icon={x[1]}
|
||||
label={t(`videos-map.sort-${x[0]}`)}
|
||||
selected={sort === x[0]}
|
||||
onSelect={() => setSort(x[0])}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddVideoFooter = ({
|
||||
addTitle,
|
||||
}: {
|
||||
addTitle: (title: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ComboBox
|
||||
Trigger={(props) => (
|
||||
<Button
|
||||
icon={LibraryAdd}
|
||||
text={t("videos-map.add")}
|
||||
className="m-6 mt-10"
|
||||
onPress={props.onPress ?? (props as any).onClick}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
searchPlaceholder={t("navbar.search")}
|
||||
value={null}
|
||||
query={(q) => ({
|
||||
parser: FullVideo,
|
||||
path: ["api", "videos"],
|
||||
params: {
|
||||
query: q,
|
||||
sort: "path",
|
||||
},
|
||||
infinite: true,
|
||||
})}
|
||||
getKey={(x) => x.id}
|
||||
getLabel={(x) => x.path}
|
||||
onValueChange={(x) => {
|
||||
if (x) addTitle(x.guess.title);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
130
front/src/ui/admin/videos-modal/index.tsx
Normal file
130
front/src/ui/admin/videos-modal/index.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { type Entry, FullVideo, type Page } from "~/models";
|
||||
import { IconButton, Modal, P, Skeleton, tooltip } from "~/primitives";
|
||||
import {
|
||||
InfiniteFetch,
|
||||
type QueryIdentifier,
|
||||
useFetch,
|
||||
useMutation,
|
||||
} from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Header } from "../../details/header";
|
||||
import { AddVideoFooter, SortMenu } from "./headers";
|
||||
import { PathItem } from "./path-item";
|
||||
|
||||
export const useEditLinks = (
|
||||
slug: string,
|
||||
titles: string[],
|
||||
sort: "path" | "entry",
|
||||
) => {
|
||||
const { mutateAsync } = useMutation({
|
||||
method: "PUT",
|
||||
path: ["api", "videos", "link"],
|
||||
compute: ({
|
||||
video,
|
||||
entries,
|
||||
guess = false,
|
||||
}: {
|
||||
video: string;
|
||||
entries: Omit<Entry, "href" | "progress" | "videos">[];
|
||||
guess?: boolean;
|
||||
}) => ({
|
||||
body: [
|
||||
{
|
||||
id: video,
|
||||
for: entries.map((x) =>
|
||||
guess && x.kind === "episode"
|
||||
? {
|
||||
serie: slug,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
season: x.seasonNumber,
|
||||
// @ts-expect-error: idk why it couldn't match x as an episode
|
||||
episode: x.episodeNumber,
|
||||
}
|
||||
: { slug: x.slug },
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
invalidate: ["api", "series", slug],
|
||||
optimisticKey: VideosModal.query(slug, titles, sort),
|
||||
optimistic: (params, prev?: { pages: Page<FullVideo>[] }) => ({
|
||||
...prev!,
|
||||
pages: prev!.pages.map((p) => ({
|
||||
...p,
|
||||
items: p!.items.map((x) => {
|
||||
if (x.id !== params.video) return x;
|
||||
return { ...x, entries: params.entries };
|
||||
}) as FullVideo[],
|
||||
})),
|
||||
}),
|
||||
});
|
||||
return mutateAsync;
|
||||
};
|
||||
|
||||
export const VideosModal = () => {
|
||||
const [slug] = useQueryState<string>("slug", undefined!);
|
||||
const { data } = useFetch(Header.query("serie", slug));
|
||||
const { t } = useTranslation();
|
||||
const [titles, setTitles] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<"entry" | "path">("path");
|
||||
const editLinks = useEditLinks(slug, titles, sort);
|
||||
|
||||
const addTitle = (x: string) => {
|
||||
if (!titles.includes(x)) setTitles([...titles, x]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={data?.name ?? t("misc.loading")} scroll={false}>
|
||||
{[...titles].map((title) => (
|
||||
<View
|
||||
key={title}
|
||||
className="m-2 flex-row items-center justify-between rounded bg-card px-6"
|
||||
>
|
||||
<P>{t("videos-map.related", { title })}</P>
|
||||
<IconButton
|
||||
icon={Close}
|
||||
onPress={() => {
|
||||
setTitles(titles.filter((x) => x !== title));
|
||||
}}
|
||||
{...tooltip(t("misc.cancel"))}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
<View className="mx-6 mb-6 flex-row items-center">
|
||||
<SortMenu sort={sort} setSort={setSort} />
|
||||
</View>
|
||||
<InfiniteFetch
|
||||
query={VideosModal.query(slug, titles, sort)}
|
||||
layout={{ layout: "vertical", gap: 8, numColumns: 1, size: 48 }}
|
||||
Render={({ item }) => (
|
||||
<PathItem
|
||||
item={item}
|
||||
serieSlug={slug}
|
||||
addTitle={addTitle}
|
||||
editLinks={editLinks}
|
||||
/>
|
||||
)}
|
||||
Loader={() => <Skeleton />}
|
||||
Footer={<AddVideoFooter addTitle={addTitle} />}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
VideosModal.query = (
|
||||
slug: string,
|
||||
titles: string[],
|
||||
sort: "path" | "entry",
|
||||
): QueryIdentifier<FullVideo> => ({
|
||||
parser: FullVideo,
|
||||
path: ["api", "series", slug, "videos"],
|
||||
params: {
|
||||
sort: sort === "entry" ? ["entry", "path"] : sort,
|
||||
titles: titles,
|
||||
},
|
||||
infinite: true,
|
||||
});
|
||||
1
front/src/ui/admin/videos-modal/mutate.ts
Normal file
1
front/src/ui/admin/videos-modal/mutate.ts
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
109
front/src/ui/admin/videos-modal/path-item.tsx
Normal file
109
front/src/ui/admin/videos-modal/path-item.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import Question from "@material-symbols/svg-400/rounded/question_mark-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { entryDisplayNumber } from "~/components/entries";
|
||||
import { Entry, type FullVideo } from "~/models";
|
||||
import { ComboBox, IconButton, P, tooltip } from "~/primitives";
|
||||
import { uniqBy } from "~/utils";
|
||||
import type { useEditLinks } from ".";
|
||||
|
||||
export const PathItem = ({
|
||||
item,
|
||||
serieSlug,
|
||||
addTitle,
|
||||
editLinks,
|
||||
}: {
|
||||
item: FullVideo;
|
||||
serieSlug: string;
|
||||
addTitle: (title: string) => void;
|
||||
editLinks: ReturnType<typeof useEditLinks>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const saved = item.entries.length;
|
||||
const guess = !saved
|
||||
? uniqBy(
|
||||
item.guess.episodes.map(
|
||||
(x) =>
|
||||
({
|
||||
kind: "episode",
|
||||
id: `s${x.season}-e${x.episode}`,
|
||||
seasonNumber: x.season,
|
||||
episodeNumber: x.episode,
|
||||
}) as Entry,
|
||||
),
|
||||
(x) => x.id,
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<View
|
||||
className="mx-6 h-12 flex-row items-center justify-between hover:bg-card"
|
||||
style={!saved && { opacity: 0.6 }}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
{saved ? (
|
||||
<IconButton
|
||||
icon={Close}
|
||||
onPress={async () => {
|
||||
addTitle(item.guess.title);
|
||||
await editLinks({ video: item.id, entries: [] });
|
||||
}}
|
||||
{...tooltip(t("videos-map.delete"))}
|
||||
/>
|
||||
) : guess.length ? (
|
||||
<IconButton
|
||||
icon={Check}
|
||||
onPress={async () => {
|
||||
await editLinks({
|
||||
video: item.id,
|
||||
entries: guess,
|
||||
guess: true,
|
||||
});
|
||||
}}
|
||||
{...tooltip(t("videos-map.validate"))}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled
|
||||
icon={Question}
|
||||
{...tooltip(t("videos-map.no-guess"))}
|
||||
/>
|
||||
)}
|
||||
<P>{item.path}</P>
|
||||
</View>
|
||||
<ComboBox
|
||||
multiple
|
||||
label={t("videos-map.none")}
|
||||
searchPlaceholder={t("navbar.search")}
|
||||
values={saved ? item.entries : guess}
|
||||
query={(q) => ({
|
||||
parser: Entry,
|
||||
path: ["api", "series", serieSlug, "entries"],
|
||||
params: {
|
||||
query: q,
|
||||
},
|
||||
infinite: true,
|
||||
})}
|
||||
getKey={
|
||||
saved
|
||||
? (x) => x.id
|
||||
: (x) =>
|
||||
x.kind === "episode"
|
||||
? `${x.seasonNumber}-${x.episodeNumber}`
|
||||
: x.id
|
||||
}
|
||||
getLabel={(x) => `${entryDisplayNumber(x)} - ${x.name}`}
|
||||
getSmallLabel={entryDisplayNumber}
|
||||
onValueChange={async (entries) => {
|
||||
if (!entries.length) addTitle(item.guess.title);
|
||||
await editLinks({
|
||||
video: item.id,
|
||||
entries,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user