mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-08-07 09:01:29 -04:00
Use rnv v7 for the rework
This commit is contained in:
parent
0a992273bf
commit
012fb930f2
@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mobile",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "expo-router/entry",
|
|
||||||
"sideEffects": false,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "expo start",
|
|
||||||
"android": "expo run:android",
|
|
||||||
"ios": "expo run:ios",
|
|
||||||
"web": "expo start --web",
|
|
||||||
"build": "eas build --profile production --platform android --non-interactive --auto-submit",
|
|
||||||
"build:apk": "eas build --profile preview --platform android --non-interactive --json",
|
|
||||||
"build:dev": "eas build --profile development --platform android --non-interactive",
|
|
||||||
"update": "eas update --auto --channel prod"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@expo-google-fonts/poppins": "^0.2.3",
|
|
||||||
"@formatjs/intl-displaynames": "^6.6.8",
|
|
||||||
"@formatjs/intl-locale": "^4.0.0",
|
|
||||||
"@gorhom/portal": "^1.0.14",
|
|
||||||
"@kesha-antonov/react-native-background-downloader": "^3.2.0",
|
|
||||||
"@kyoo/ui": "workspace:^",
|
|
||||||
"@material-symbols/svg-400": "^0.22.0",
|
|
||||||
"@react-native-community/netinfo": "11.3.2",
|
|
||||||
"@shopify/flash-list": "1.7.1",
|
|
||||||
"@tanstack/query-sync-storage-persister": "^5.51.21",
|
|
||||||
"@tanstack/react-query": "^5.51.23",
|
|
||||||
"@tanstack/react-query-persist-client": "^5.51.23",
|
|
||||||
"array-shuffle": "^3.0.0",
|
|
||||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
|
||||||
"expo": "^51.0.26",
|
|
||||||
"expo-build-properties": "~0.12.5",
|
|
||||||
"expo-constants": "~16.0.2",
|
|
||||||
"expo-dev-client": "~4.0.22",
|
|
||||||
"expo-file-system": "~17.0.1",
|
|
||||||
"expo-font": "~12.0.9",
|
|
||||||
"expo-image-picker": "~15.0.7",
|
|
||||||
"expo-linear-gradient": "~13.0.2",
|
|
||||||
"expo-linking": "~6.3.1",
|
|
||||||
"expo-localization": "~15.0.3",
|
|
||||||
"expo-navigation-bar": "~3.0.7",
|
|
||||||
"expo-router": "3.5.21",
|
|
||||||
"expo-screen-orientation": "~7.0.5",
|
|
||||||
"expo-secure-store": "~13.0.2",
|
|
||||||
"expo-status-bar": "~1.12.1",
|
|
||||||
"expo-updates": "~0.25.22",
|
|
||||||
"i18next": "^23.12.2",
|
|
||||||
"intl-pluralrules": "^2.0.1",
|
|
||||||
"moti": "^0.29.0",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-i18next": "^15.0.1",
|
|
||||||
"react-native": "0.74.5",
|
|
||||||
"react-native-blurhash": "^2.0.3",
|
|
||||||
"react-native-fast-image": "^8.6.3",
|
|
||||||
"react-native-mmkv": "^2.12.2",
|
|
||||||
"react-native-reanimated": "~3.15.0",
|
|
||||||
"react-native-safe-area-context": "4.10.8",
|
|
||||||
"react-native-screens": "3.34.0",
|
|
||||||
"react-native-svg": "15.2.0",
|
|
||||||
"react-native-uuid": "^2.0.2",
|
|
||||||
"react-native-video": "^6.4.3",
|
|
||||||
"yoshiki": "1.2.14"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.25.2",
|
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
|
||||||
"typescript": "~5.5.4"
|
|
||||||
},
|
|
||||||
"installConfig": {
|
|
||||||
"hoistingLimits": "workspaces"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "web",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"sideEffects": ["./src/polyfill.ts"],
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"format": "prettier --check --ignore-path .gitignore '!src/utils/jotai-utils.tsx' .",
|
|
||||||
"format:fix": "prettier --write --ignore-path .gitignore '!src/utils/jotai-utils.tsx' ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@gorhom/portal": "^1.0.14",
|
|
||||||
"@kyoo/models": "workspace:^",
|
|
||||||
"@kyoo/primitives": "workspace:^",
|
|
||||||
"@kyoo/ui": "workspace:^",
|
|
||||||
"@material-symbols/svg-400": "^0.22.0",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
|
||||||
"@tanstack/react-query": "^5.51.23",
|
|
||||||
"@tanstack/react-query-devtools": "^5.51.23",
|
|
||||||
"array-shuffle": "^3.0.0",
|
|
||||||
"expo-image-picker": "~15.0.7",
|
|
||||||
"expo-linear-gradient": "^13.0.2",
|
|
||||||
"expo-modules-core": "^1.12.20",
|
|
||||||
"hls.js": "^1.5.14",
|
|
||||||
"i18next": "^23.12.2",
|
|
||||||
"jassub": "1.7.15",
|
|
||||||
"jotai": "^2.9.2",
|
|
||||||
"moti": "^0.29.0",
|
|
||||||
"next": "14.2.5",
|
|
||||||
"next-translate": "^2.6.2",
|
|
||||||
"raf": "^3.4.1",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"react-i18next": "^15.0.1",
|
|
||||||
"react-native-reanimated": "3.15.0",
|
|
||||||
"react-native-svg": "15.2.0",
|
|
||||||
"react-native-video": "^6.4.3",
|
|
||||||
"react-native-web": "0.19.12",
|
|
||||||
"react-tooltip": "^5.28.0",
|
|
||||||
"solito": "^4.2.2",
|
|
||||||
"srt-webvtt": "zoriya/srt-webvtt#build",
|
|
||||||
"superjson": "^2.2.1",
|
|
||||||
"sweetalert2": "^11.12.4",
|
|
||||||
"yoshiki": "1.2.14",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@svgr/webpack": "^8.1.0",
|
|
||||||
"@types/node": "22.2.0",
|
|
||||||
"@types/react-dom": "18.3.0",
|
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
|
||||||
"react-native": "0.74.5",
|
|
||||||
"typescript": "^5.5.4",
|
|
||||||
"webpack": "^5.93.0"
|
|
||||||
}
|
|
||||||
}
|
|
963
front/bun.lock
963
front/bun.lock
File diff suppressed because it is too large
Load Diff
@ -33,7 +33,6 @@
|
|||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-updates": "~0.28.14",
|
"expo-updates": "~0.28.14",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"jotai": "^2.12.5",
|
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-i18next": "^15.5.2",
|
"react-i18next": "^15.5.2",
|
||||||
"react-native": "0.79.3",
|
"react-native": "0.79.3",
|
||||||
@ -42,7 +41,7 @@
|
|||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
"react-native-svg": "15.11.2",
|
"react-native-svg": "15.11.2",
|
||||||
"react-native-video": "^6.15.0",
|
"react-native-video": "^7.0.0-alpha.1",
|
||||||
"react-native-web": "^0.20.0",
|
"react-native-web": "^0.20.0",
|
||||||
"react-tooltip": "^5.29.1",
|
"react-tooltip": "^5.29.1",
|
||||||
"sweetalert2": "^11.22.0",
|
"sweetalert2": "^11.22.0",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { Entry } from "./entry";
|
import { Entry } from "./entry";
|
||||||
import { Extra } from "./extra";
|
import { Extra } from "./extra";
|
||||||
|
import { Show } from "./show";
|
||||||
import { zdate } from "./utils/utils";
|
import { zdate } from "./utils/utils";
|
||||||
|
|
||||||
export const Video = z.object({
|
export const Video = z.object({
|
||||||
@ -36,9 +37,16 @@ export const Video = z.object({
|
|||||||
|
|
||||||
export const FullVideo = Video.extend({
|
export const FullVideo = Video.extend({
|
||||||
slugs: z.array(z.string()),
|
slugs: z.array(z.string()),
|
||||||
|
progress: z.object({
|
||||||
|
percent: z.int().min(0).max(100),
|
||||||
|
time: z.int().min(0).nullable(),
|
||||||
|
playedDate: zdate().nullable(),
|
||||||
|
videoId: z.string().nullable(),
|
||||||
|
}),
|
||||||
entries: z.array(Entry),
|
entries: z.array(Entry),
|
||||||
previous: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
previous: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||||
next: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
next: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||||
|
show: Show.optional(),
|
||||||
});
|
});
|
||||||
export type FullVideo = z.infer<typeof FullVideo>;
|
export type FullVideo = z.infer<typeof FullVideo>;
|
||||||
|
|
||||||
|
@ -62,3 +62,16 @@ export const readValue = <T extends ZodType>(key: string, parser: T) => {
|
|||||||
if (val === undefined) return val;
|
if (val === undefined) return val;
|
||||||
return parser.parse(JSON.parse(val)) as z.infer<T>;
|
return parser.parse(JSON.parse(val)) as z.infer<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useLocalSetting = <T extends string>(setting: string, def: T) => {
|
||||||
|
if (Platform.OS === "web" && typeof window === "undefined")
|
||||||
|
return [def as T, null!] as const;
|
||||||
|
// biome-ignore lint/correctness/useHookAtTopLevel: ssr
|
||||||
|
const [val, setter] = useMMKVString(`settings.${setting}`, storage);
|
||||||
|
return [(val ?? def) as T, setter] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocalSetting = (setting: string, def: string) => {
|
||||||
|
if (Platform.OS === "web" && typeof window === "undefined") return def;
|
||||||
|
return storage.getString(`settings.${setting}`) ?? setting;
|
||||||
|
};
|
||||||
|
@ -1,184 +1,115 @@
|
|||||||
import { useSetAtom } from "jotai";
|
import { Stack } from "expo-router";
|
||||||
import { type ComponentProps, useEffect, useState } from "react";
|
import { StyleSheet, View } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useVideoPlayer, VideoView } from "react-native-video";
|
||||||
import { Platform, StyleSheet, View } from "react-native";
|
import { entryDisplayNumber } from "~/components/entries";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { FullVideo, VideoInfo } from "~/models";
|
||||||
import { type Episode, FullVideo, type Movie, VideoInfo } from "~/models";
|
|
||||||
import { Head } from "~/primitives";
|
import { Head } from "~/primitives";
|
||||||
import type { QueryIdentifier } from "~/query";
|
import { useToken } from "~/providers/account-context";
|
||||||
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
import { useLocalSetting } from "~/providers/settings";
|
||||||
import { useVideoKeyboard } from "./keyboard";
|
import { type QueryIdentifier, useFetch } from "~/query";
|
||||||
import { durationAtom, fullscreenAtom, Video } from "./state";
|
import { useQueryState } from "~/utils";
|
||||||
import { WatchStatusObserver } from "./watch-status-observer";
|
|
||||||
|
|
||||||
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
|
// import { Hover, LoadingIndicator } from "./components/hover";
|
||||||
|
// import { useVideoKeyboard } from "./keyboard";
|
||||||
|
// import { durationAtom, fullscreenAtom, Video } from "./state";
|
||||||
|
|
||||||
|
const mapMetadata = (item: FullVideo | undefined) => {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
// TODO: map current entry using entries' duration & the current playtime
|
||||||
|
const currentEntry = 0;
|
||||||
|
const entry = item.entries[currentEntry] ?? item.entries[0];
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
const mapData = (
|
|
||||||
data: Item | undefined,
|
|
||||||
info: WatchInfo | undefined,
|
|
||||||
previousSlug?: string,
|
|
||||||
nextSlug?: string,
|
|
||||||
): Partial<ComponentProps<typeof Hover>> & { isLoading: boolean } => {
|
|
||||||
if (!data) return { isLoading: true };
|
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
currentEntry,
|
||||||
name:
|
title: `${entry.name} (${entryDisplayNumber(entry)})`,
|
||||||
data.type === "movie"
|
description: entry.description,
|
||||||
? data.name
|
subtitle: item.show!.kind !== "movie" ? item.show!.name : null,
|
||||||
: `${episodeDisplayNumber(data)} ${data.name}`,
|
poster: item.show!.poster,
|
||||||
showName: data.type === "movie" ? data.name! : data.show!.name,
|
thumbnail: item.show!.thumbnail,
|
||||||
poster: data.type === "movie" ? data.poster : data.show!.poster,
|
|
||||||
subtitles: info?.subtitles,
|
|
||||||
audios: info?.audios,
|
|
||||||
chapters: info?.chapters,
|
|
||||||
fonts: info?.fonts,
|
|
||||||
previousSlug,
|
|
||||||
nextSlug,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTitleMetadata = (item: Item) => {
|
export const Player = () => {
|
||||||
if (item.type === "movie") {
|
const [slug] = useQueryState("slug", undefined!);
|
||||||
return item.name;
|
|
||||||
}
|
|
||||||
return `${item.name} (${episodeDisplayNumber({
|
|
||||||
seasonNumber: item.seasonNumber,
|
|
||||||
episodeNumber: item.episodeNumber,
|
|
||||||
absoluteNumber: item.absoluteNumber,
|
|
||||||
})})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Player = ({
|
const { apiUrl, authToken } = useToken();
|
||||||
slug,
|
const [playMode] = useLocalSetting<"direct" | "hls">("playMode", "direct");
|
||||||
type,
|
const player = useVideoPlayer({
|
||||||
t: startTimeP,
|
uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}`,
|
||||||
}: {
|
headers: {
|
||||||
slug: string;
|
Authorization: `Bearer ${authToken}`,
|
||||||
type: "episode" | "movie";
|
},
|
||||||
t?: number;
|
});
|
||||||
}) => {
|
|
||||||
const { css } = useYoshiki();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [playbackError, setPlaybackError] = useState<string | undefined>(
|
const { data, error } = useFetch(Player.query(slug));
|
||||||
undefined,
|
const { data: info, error: infoError } = useFetch(Player.infoQuery(slug));
|
||||||
);
|
const metadata = mapMetadata(data);
|
||||||
const { data, error } = useFetch(Player.query(type, slug));
|
|
||||||
const { data: info, error: infoError } = useFetch(
|
|
||||||
Player.infoQuery(type, slug),
|
|
||||||
);
|
|
||||||
const image =
|
|
||||||
data && data.type === "episode"
|
|
||||||
? (data.show?.poster ?? data?.poster)
|
|
||||||
: data?.poster;
|
|
||||||
const previous =
|
|
||||||
data && data.type === "episode" && data.previousEpisode
|
|
||||||
? `/watch/${data.previousEpisode.slug}?t=0`
|
|
||||||
: undefined;
|
|
||||||
const next =
|
|
||||||
data && data.type === "episode" && data.nextEpisode
|
|
||||||
? `/watch/${data.nextEpisode.slug}?t=0`
|
|
||||||
: undefined;
|
|
||||||
const title = data && formatTitleMetadata(data);
|
|
||||||
const subtitle =
|
|
||||||
data && data.type === "episode" ? data.show?.name : undefined;
|
|
||||||
|
|
||||||
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
// const [playbackError, setPlaybackError] = useState<string | undefined>(
|
||||||
|
// undefined,
|
||||||
|
// );
|
||||||
|
// useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||||
|
|
||||||
const startTime = startTimeP ?? data?.watchStatus?.watchedTime;
|
// const startTime = startTimeP ?? data?.watchStatus?.watchedTime;
|
||||||
|
|
||||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
// const setFullscreen = useSetAtom(fullscreenAtom);
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (Platform.OS !== "web") return;
|
// if (Platform.OS !== "web") return;
|
||||||
if (/Mobi/i.test(window.navigator.userAgent)) setFullscreen(true);
|
// if (/Mobi/i.test(window.navigator.userAgent)) setFullscreen(true);
|
||||||
return () => {
|
// return () => {
|
||||||
if (!document.location.href.includes("/watch")) setFullscreen(false);
|
// if (!document.location.href.includes("/watch")) setFullscreen(false);
|
||||||
};
|
// };
|
||||||
}, [setFullscreen]);
|
// }, [setFullscreen]);
|
||||||
|
|
||||||
const setDuration = useSetAtom(durationAtom);
|
// if (error || infoError || playbackError)
|
||||||
useEffect(() => {
|
// return (
|
||||||
setDuration(info?.durationSeconds);
|
// <>
|
||||||
}, [info, setDuration]);
|
// <Back
|
||||||
|
// isLoading={false}
|
||||||
if (error || infoError || playbackError)
|
// {...css({ position: "relative", bg: (theme) => theme.accent })}
|
||||||
return (
|
// />
|
||||||
<>
|
// <ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} />
|
||||||
<Back
|
// </>
|
||||||
isLoading={false}
|
// );
|
||||||
{...css({ position: "relative", bg: (theme) => theme.accent })}
|
|
||||||
/>
|
|
||||||
<ErrorView error={error ?? infoError ?? { errors: [playbackError!] }} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
<Head title={title} description={data?.overview} />
|
style={{
|
||||||
{data && info && (
|
flex: 1,
|
||||||
<WatchStatusObserver
|
backgroundColor: "black",
|
||||||
type={type}
|
}}
|
||||||
slug={data.slug}
|
>
|
||||||
duration={info.durationSeconds}
|
<Head
|
||||||
/>
|
title={metadata?.title}
|
||||||
)}
|
description={metadata?.description}
|
||||||
<View
|
image={metadata?.thumbnail?.high}
|
||||||
{...css({
|
/>
|
||||||
flexGrow: 1,
|
<Stack.Screen
|
||||||
flexShrink: 1,
|
options={{
|
||||||
bg: "black",
|
// TODO: find a way to force fullscreen on mobile
|
||||||
})}
|
headerTransparent: true,
|
||||||
>
|
headerStyle: { backgroundColor: undefined },
|
||||||
<Video
|
}}
|
||||||
metadata={{
|
/>
|
||||||
title: title ?? t("show.episodeNoMetadata"),
|
<VideoView
|
||||||
artist: subtitle ?? undefined,
|
player={player}
|
||||||
description: data?.overview ?? undefined,
|
pictureInPicture={true}
|
||||||
imageUri: image?.medium,
|
autoEnterPictureInPicture={true}
|
||||||
next: next,
|
resizeMode={"contain"}
|
||||||
previous: previous,
|
style={StyleSheet.absoluteFillObject}
|
||||||
}}
|
/>
|
||||||
links={data?.links}
|
{/* <LoadingIndicator /> */}
|
||||||
audios={info?.audios}
|
{/* <Hover {...mapData(data, info, previous, next)} url={`${type}/${slug}`} /> */}
|
||||||
subtitles={info?.subtitles}
|
</View>
|
||||||
codec={info?.mimeCodec}
|
|
||||||
setError={setPlaybackError}
|
|
||||||
fonts={info?.fonts}
|
|
||||||
startTime={startTime}
|
|
||||||
onEnd={() => {
|
|
||||||
if (!data) return;
|
|
||||||
if (data.type === "movie")
|
|
||||||
router.replace(`/movie/${data.slug}`, undefined, {
|
|
||||||
experimental: {
|
|
||||||
nativeBehavior: "stack-replace",
|
|
||||||
isNestedNavigator: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
else
|
|
||||||
router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
|
|
||||||
experimental: {
|
|
||||||
nativeBehavior: "stack-replace",
|
|
||||||
isNestedNavigator: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
{...css(StyleSheet.absoluteFillObject)}
|
|
||||||
/>
|
|
||||||
<LoadingIndicator />
|
|
||||||
<Hover
|
|
||||||
{...mapData(data, info, previous, next)}
|
|
||||||
url={`${type}/${slug}`}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Player.query = (slug: string): QueryIdentifier<FullVideo> => ({
|
Player.query = (slug: string): QueryIdentifier<FullVideo> => ({
|
||||||
path: ["api", "videos", slug],
|
path: ["api", "videos", slug],
|
||||||
params: {
|
params: {
|
||||||
fields: ["next", "previous", "serie"],
|
fields: ["next", "previous", "show"],
|
||||||
},
|
},
|
||||||
parser: FullVideo,
|
parser: FullVideo,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user