Init player rework

This commit is contained in:
Zoe Roux 2025-07-19 18:17:48 +02:00
parent 6b82053aec
commit c1ee6b9821
No known key found for this signature in database
15 changed files with 90 additions and 85 deletions

View File

@ -21,7 +21,7 @@
import { type WatchInfo, getCurrentApiUrl, queryFn, toQueryKey } from "@kyoo/models"; import { type WatchInfo, getCurrentApiUrl, queryFn, toQueryKey } from "@kyoo/models";
import { getCurrentAccount } from "@kyoo/models/src/account-internal"; import { getCurrentAccount } from "@kyoo/models/src/account-internal";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Player } from "../player"; import { Player } from "../../../../src/ui/player../src/ui/player";
export const useDownloader = () => { export const useDownloader = () => {
return async (type: "episode" | "movie", slug: string) => { return async (type: "episode" | "movie", slug: string) => {

View File

@ -41,7 +41,7 @@ import { type PrimitiveAtom, atom, useSetAtom, useStore } from "jotai";
import { type ReactNode, useEffect } from "react"; import { type ReactNode, useEffect } from "react";
import { ToastAndroid } from "react-native"; import { ToastAndroid } from "react-native";
import { z } from "zod"; import { z } from "zod";
import { Player } from "../player"; import { Player } from "../../../../src/ui/player";
type Router = ReturnType<typeof useRouter>; type Router = ReturnType<typeof useRouter>;

View File

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

View File

@ -35,6 +35,7 @@ import {
A, A,
Chip, Chip,
Container, Container,
ContrastArea,
capitalize, capitalize,
DottedSeparator, DottedSeparator,
GradientImageBackground, GradientImageBackground,
@ -714,30 +715,32 @@ export const Header = ({
}, },
}) as any)} }) as any)}
/> />
<TitleLine <ContrastArea>
kind={kind} <TitleLine
slug={slug} kind={kind}
name={data.name} slug={slug}
tagline={data.tagline} name={data.name}
date={getDisplayDate(data)} tagline={data.tagline}
rating={data.rating} date={getDisplayDate(data)}
runtime={data.kind === "movie" ? data.runtime : null} rating={data.rating}
poster={data.poster} runtime={data.kind === "movie" ? data.runtime : null}
studios={data.kind !== "collection" ? data.studios! : null} poster={data.poster}
playHref={data.kind !== "collection" ? data.playHref : null} studios={data.kind !== "collection" ? data.studios! : null}
trailerUrl={data.kind !== "collection" ? data.trailerUrl : null} playHref={data.kind !== "collection" ? data.playHref : null}
watchStatus={ trailerUrl={data.kind !== "collection" ? data.trailerUrl : null}
data.kind !== "collection" ? data.watchStatus?.status! : null watchStatus={
} data.kind !== "collection" ? data.watchStatus?.status! : null
{...css({ }
marginTop: { {...css({
xs: max(vh(20), px(200)), marginTop: {
sm: vh(45), xs: max(vh(20), px(200)),
md: max(vh(30), px(150)), sm: vh(45),
lg: max(vh(35), px(200)), md: max(vh(30), px(150)),
}, lg: max(vh(35), px(200)),
})} },
/> })}
/>
</ContrastArea>
<Description <Description
description={data?.description} description={data?.description}
genres={data?.genres} genres={data?.genres}

View File

@ -29,7 +29,7 @@ import { useAtom } from "jotai";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { type Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, useYoshiki } from "yoshiki/native";
import { useSubtitleName } from "../../utils"; import { useSubtitleName } from "../../../../packages/ui/src/utils";
import { fullscreenAtom, subtitleAtom } from "../state"; import { fullscreenAtom, subtitleAtom } from "../state";
import { AudiosMenu, QualitiesMenu } from "../video"; import { AudiosMenu, QualitiesMenu } from "../video";

View File

@ -24,7 +24,7 @@ import { useAtomValue } from "jotai";
import { useMemo } from "react"; import { useMemo } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { type Theme, percent, px, useForceRerender, useYoshiki } from "yoshiki/native"; import { type Theme, percent, px, useForceRerender, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../../../../../src/ui/errors"; import { ErrorView } from "../../errors";
import { durationAtom } from "../state"; import { durationAtom } from "../state";
import { seekProgressAtom } from "./hover"; import { seekProgressAtom } from "./hover";
import { toTimerString } from "./left-buttons"; import { toTimerString } from "./left-buttons";

View File

@ -1,45 +1,20 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import {
type Episode,
EpisodeP,
type Movie,
MovieP,
type QueryIdentifier,
type WatchInfo,
WatchInfoP,
useFetch,
} from "@kyoo/models";
import { Head } from "@kyoo/primitives";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { type ComponentProps, useEffect, useState } from "react"; import { type ComponentProps, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, StyleSheet, View } from "react-native"; import { Platform, StyleSheet, View } from "react-native";
import { useRouter } from "solito/router";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { episodeDisplayNumber } from "../../../../src/ui/details/episode"; import {
import { ErrorView } from "../../../../src/ui/errors"; Episode,
Movie,
type QueryIdentifier,
useFetch,
type WatchInfo,
WatchInfoP,
} from "~/models";
import { Head } from "~/primitives";
import { Back, Hover, LoadingIndicator } from "./components/hover"; import { Back, Hover, LoadingIndicator } from "./components/hover";
import { useVideoKeyboard } from "./keyboard"; import { useVideoKeyboard } from "./keyboard";
import { Video, durationAtom, fullscreenAtom } from "./state"; import { durationAtom, fullscreenAtom, Video } from "./state";
import { WatchStatusObserver } from "./watch-status-observer"; import { WatchStatusObserver } from "./watch-status-observer";
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" }); type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
@ -53,7 +28,10 @@ const mapData = (
if (!data) return { isLoading: true }; if (!data) return { isLoading: true };
return { return {
isLoading: false, isLoading: false,
name: data.type === "movie" ? data.name : `${episodeDisplayNumber(data)} ${data.name}`, name:
data.type === "movie"
? data.name
: `${episodeDisplayNumber(data)} ${data.name}`,
showName: data.type === "movie" ? data.name! : data.show!.name, showName: data.type === "movie" ? data.name! : data.show!.name,
poster: data.type === "movie" ? data.poster : data.show!.poster, poster: data.type === "movie" ? data.poster : data.show!.poster,
subtitles: info?.subtitles, subtitles: info?.subtitles,
@ -89,11 +67,17 @@ export const Player = ({
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined); const [playbackError, setPlaybackError] = useState<string | undefined>(
undefined,
);
const { data, error } = useFetch(Player.query(type, slug)); const { data, error } = useFetch(Player.query(type, slug));
const { data: info, error: infoError } = useFetch(Player.infoQuery(type, slug)); const { data: info, error: infoError } = useFetch(
Player.infoQuery(type, slug),
);
const image = const image =
data && data.type === "episode" ? (data.show?.poster ?? data?.poster) : data?.poster; data && data.type === "episode"
? (data.show?.poster ?? data?.poster)
: data?.poster;
const previous = const previous =
data && data.type === "episode" && data.previousEpisode data && data.type === "episode" && data.previousEpisode
? `/watch/${data.previousEpisode.slug}?t=0` ? `/watch/${data.previousEpisode.slug}?t=0`
@ -103,7 +87,8 @@ export const Player = ({
? `/watch/${data.nextEpisode.slug}?t=0` ? `/watch/${data.nextEpisode.slug}?t=0`
: undefined; : undefined;
const title = data && formatTitleMetadata(data); const title = data && formatTitleMetadata(data);
const subtitle = data && data.type === "episode" ? data.show?.name : undefined; const subtitle =
data && data.type === "episode" ? data.show?.name : undefined;
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next); useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
@ -126,7 +111,10 @@ export const Player = ({
if (error || infoError || playbackError) if (error || infoError || playbackError)
return ( return (
<> <>
<Back isLoading={false} {...css({ position: "relative", bg: (theme) => theme.accent })} /> <Back
isLoading={false}
{...css({ position: "relative", bg: (theme) => theme.accent })}
/>
<ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} /> <ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} />
</> </>
); );
@ -135,7 +123,11 @@ export const Player = ({
<> <>
<Head title={title} description={data?.overview} /> <Head title={title} description={data?.overview} />
{data && info && ( {data && info && (
<WatchStatusObserver type={type} slug={data.slug} duration={info.durationSeconds} /> <WatchStatusObserver
type={type}
slug={data.slug}
duration={info.durationSeconds}
/>
)} )}
<View <View
{...css({ {...css({
@ -164,23 +156,35 @@ export const Player = ({
if (!data) return; if (!data) return;
if (data.type === "movie") if (data.type === "movie")
router.replace(`/movie/${data.slug}`, undefined, { router.replace(`/movie/${data.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true }, experimental: {
nativeBehavior: "stack-replace",
isNestedNavigator: true,
},
}); });
else else
router.replace(next ?? `/show/${data.show!.slug}`, undefined, { router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true }, experimental: {
nativeBehavior: "stack-replace",
isNestedNavigator: true,
},
}); });
}} }}
{...css(StyleSheet.absoluteFillObject)} {...css(StyleSheet.absoluteFillObject)}
/> />
<LoadingIndicator /> <LoadingIndicator />
<Hover {...mapData(data, info, previous, next)} url={`${type}/${slug}`} /> <Hover
{...mapData(data, info, previous, next)}
url={`${type}/${slug}`}
/>
</View> </View>
</> </>
); );
}; };
Player.query = (type: "episode" | "movie", slug: string): QueryIdentifier<Item> => Player.query = (
type: "episode" | "movie",
slug: string,
): QueryIdentifier<Item> =>
type === "episode" type === "episode"
? { ? {
path: ["episode", slug], path: ["episode", slug],
@ -197,15 +201,10 @@ Player.query = (type: "episode" | "movie", slug: string): QueryIdentifier<Item>
parser: MovieP.transform((x) => ({ ...x, type: "movie" })), parser: MovieP.transform((x) => ({ ...x, type: "movie" })),
}; };
Player.infoQuery = (type: "episode" | "movie", slug: string): QueryIdentifier<WatchInfo> => ({ Player.infoQuery = (
type: "episode" | "movie",
slug: string,
): QueryIdentifier<WatchInfo> => ({
path: [type, slug, "info"], path: [type, slug, "info"],
parser: WatchInfoP, parser: WatchInfoP,
}); });
// if more queries are needed, dont forget to update download.tsx to cache those.
Player.getFetchUrls = ({ slug, type }: { slug: string; type: "episode" | "movie" }) => [
Player.query(type, slug),
Player.infoQuery(type, slug),
];
Player.requiredPermissions = ["overall.play"];

View File

@ -50,7 +50,7 @@ import NativeVideo, {
SelectedVideoTrackType, SelectedVideoTrackType,
} from "react-native-video"; } from "react-native-video";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { useDisplayName } from "../utils"; import { useDisplayName } from "../../../packages/ui/src/utils";
import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./state"; import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./state";
const MimeTypes: Map<string, string> = new Map([ const MimeTypes: Map<string, string> = new Map([

View File

@ -36,7 +36,7 @@ import { useTranslation } from "react-i18next";
import type { VideoProps } from "react-native-video"; import type { VideoProps } from "react-native-video";
import toVttBlob from "srt-webvtt"; import toVttBlob from "srt-webvtt";
import { useForceRerender, useYoshiki } from "yoshiki"; import { useForceRerender, useYoshiki } from "yoshiki";
import { useDisplayName } from "../utils"; import { useDisplayName } from "../../../packages/ui/src/utils";
import { MediaSessionManager } from "./media-session"; import { MediaSessionManager } from "./media-session";
import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state"; import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state";