diff --git a/front/public/translations/en.json b/front/public/translations/en.json index 6fe0a950..e7992e29 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -49,7 +49,7 @@ "part": "Part {{number}}", "videos-map": "Edit video mappings", "remap": "Remap", - "staff-as":"as {{character}}", + "staff-as": "as {{character}}", "staff-kind": { "actor": "Actor", "director": "Director", @@ -253,7 +253,9 @@ "transmux": "Original", "auto": "Auto", "notInPristine": "Unavailable in pristine", - "unsupportedError": "Video codec not supported, transcoding in progress..." + "unsupportedError": "Video codec not supported, transcoding in progress...", + "not-available": "{{entry}} is not available on kyoo yet, ask your server admins about it", + "fatal": "Fatal playback error" }, "search": { "empty": "No result found. Try a different query." diff --git a/front/src/models/video.ts b/front/src/models/video.ts index e5ae2ee9..e06b5334 100644 --- a/front/src/models/video.ts +++ b/front/src/models/video.ts @@ -49,8 +49,14 @@ export const FullVideo = Video.extend({ 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(), + previous: z + .object({ video: z.string().nullable(), entry: Entry }) + .nullable() + .optional(), + next: z + .object({ video: z.string().nullable(), entry: Entry }) + .nullable() + .optional(), show: Show.optional().nullable(), }); export type FullVideo = z.infer; diff --git a/front/src/ui/player/controls/error-popup.tsx b/front/src/ui/player/controls/error-popup.tsx new file mode 100644 index 00000000..651bfc9a --- /dev/null +++ b/front/src/ui/player/controls/error-popup.tsx @@ -0,0 +1,29 @@ +import Close from "@material-symbols/svg-400/rounded/close-fill.svg"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { Heading, IconButton, P } from "~/primitives"; +import { cn } from "~/utils"; + +export const ErrorPopup = ({ + message, + dismiss, +}: { + message: string; + dismiss: () => void; +}) => { + const { t } = useTranslation(); + return ( + + + {t("player.fatal")} +

{message}

+
+ +
+ ); +}; diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index 1a29b047..cc13e608 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -20,6 +20,7 @@ export const Controls = ({ chapters, playPrev, playNext, + forceShow, }: { player: VideoPlayer; showHref?: string; @@ -31,6 +32,7 @@ export const Controls = ({ chapters: Chapter[]; playPrev: (() => boolean) | null; playNext: (() => boolean) | null; + forceShow?: boolean; }) => { const isTouch = useIsTouch(); @@ -56,7 +58,7 @@ export const Controls = ({ { ); const playModeState = useState(defaultPlayMode); const [playMode, setPlayMode] = playModeState; + const [playbackError, setPlaybackError] = useState(); const player = useVideoPlayer( { uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`, @@ -101,18 +104,39 @@ export const Player = () => { }, [player, info?.fonts]); const router = useRouter(); + const { t } = useTranslation(); const playPrev = useCallback(() => { if (!data?.previous) return false; + if (!data.previous.video) { + setPlaybackError({ + status: "not-available", + message: t("player.not-available", { + entry: `${entryDisplayNumber(data.previous.entry)} ${data.previous.entry.name}`, + }), + }); + return true; + } + setPlaybackError(undefined); setStart("0"); setSlug(data.previous.video); return true; - }, [data?.previous, setSlug, setStart]); + }, [data?.previous, setSlug, setStart, t]); const playNext = useCallback(() => { if (!data?.next) return false; + if (!data.next.video) { + setPlaybackError({ + status: "not-available", + message: t("player.not-available", { + entry: `${entryDisplayNumber(data.next.entry)} ${data.next.entry.name}`, + }), + }); + return true; + } + setPlaybackError(undefined); setStart("0"); setSlug(data.next.video); return true; - }, [data?.next, setSlug, setStart]); + }, [data?.next, setSlug, setStart, t]); useProgressObserver( player, @@ -148,7 +172,6 @@ export const Player = () => { }; }, []); - const [playbackError, setPlaybackError] = useState(); useEvent(player, "onError", (error) => { if ( error.code === "source/unsupported-content-type" && @@ -157,9 +180,6 @@ export const Player = () => { setPlayMode("hls"); else setPlaybackError({ status: error.code, message: error.message }); }); - if (playbackError) { - throw playbackError; - } return ( @@ -201,10 +221,17 @@ export const Player = () => { : data?.path } chapters={info?.chapters ?? []} - playPrev={data?.previous?.video ? playPrev : null} - playNext={data?.next?.video ? playNext : null} + playPrev={data?.previous ? playPrev : null} + playNext={data?.next ? playNext : null} + forceShow={!!playbackError} /> + {playbackError && ( + setPlaybackError(undefined)} + /> + )} ); };