mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Manually fix translations and update node packages (#502)
This commit is contained in:
commit
c8cc2fc057
@ -26,10 +26,6 @@ import {
|
||||
Poppins_900Black,
|
||||
useFonts,
|
||||
} from "@expo-google-fonts/poppins";
|
||||
import "@formatjs/intl-displaynames/locale-data/en";
|
||||
import "@formatjs/intl-displaynames/locale-data/fr";
|
||||
import "@formatjs/intl-displaynames/polyfill";
|
||||
import "@formatjs/intl-locale/polyfill";
|
||||
import { PortalProvider } from "@gorhom/portal";
|
||||
import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models";
|
||||
import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives";
|
||||
@ -45,14 +41,11 @@ import "intl-pluralrules";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { useColorScheme } from "react-native";
|
||||
import resources from "../../../translations";
|
||||
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { onlineManager } from "@tanstack/react-query";
|
||||
import { useTheme } from "yoshiki/native";
|
||||
// TODO: use a backend to load jsons.
|
||||
import en from "../../../translations/en.json";
|
||||
import fr from "../../../translations/fr.json";
|
||||
import zh from "../../../translations/zh.json";
|
||||
|
||||
onlineManager.setEventListener((setOnline) => {
|
||||
return NetInfo.addEventListener((state) => {
|
||||
@ -81,11 +74,7 @@ i18next.use(initReactI18next).init({
|
||||
},
|
||||
fallbackLng: "en",
|
||||
lng: getLocales()[0].languageCode ?? "en",
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
fr: { translation: fr },
|
||||
zh: { translation: zh },
|
||||
},
|
||||
resources,
|
||||
});
|
||||
|
||||
const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
@ -15,58 +15,57 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/poppins": "^0.2.3",
|
||||
"@formatjs/intl-displaynames": "^6.6.6",
|
||||
"@formatjs/intl-locale": "^3.4.5",
|
||||
"@formatjs/intl-displaynames": "^6.6.8",
|
||||
"@formatjs/intl-locale": "^4.0.0",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@kesha-antonov/react-native-background-downloader": "git+https://github.com/zoriya/react-native-background-downloader.git",
|
||||
"@kesha-antonov/react-native-background-downloader": "^3.1.3",
|
||||
"@kyoo/ui": "workspace:^",
|
||||
"@material-symbols/svg-400": "^0.14.6",
|
||||
"@react-native-community/netinfo": "11.1.0",
|
||||
"@shopify/flash-list": "1.6.3",
|
||||
"@tanstack/query-sync-storage-persister": "^5.17.19",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-query-persist-client": "^5.17.19",
|
||||
"@material-symbols/svg-400": "^0.18.0",
|
||||
"@react-native-community/netinfo": "11.3.1",
|
||||
"@shopify/flash-list": "1.6.4",
|
||||
"@tanstack/query-sync-storage-persister": "^5.37.1",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tanstack/react-query-persist-client": "^5.37.1",
|
||||
"array-shuffle": "^3.0.0",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||
"expo": "^50.0.4",
|
||||
"expo-build-properties": "~0.11.0",
|
||||
"expo-constants": "~15.4.5",
|
||||
"expo-dev-client": "~3.3.7",
|
||||
"expo-file-system": "~16.0.5",
|
||||
"expo-font": "~11.10.2",
|
||||
"expo-image-picker": "~14.7.1",
|
||||
"expo-linear-gradient": "~12.7.1",
|
||||
"expo-linking": "~6.2.2",
|
||||
"expo-localization": "~14.8.3",
|
||||
"expo-navigation-bar": "~2.8.1",
|
||||
"expo-router": "3.4.6",
|
||||
"expo-screen-orientation": "~6.4.1",
|
||||
"expo-secure-store": "~12.8.1",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"expo-updates": "~0.24.9",
|
||||
"i18next": "^23.7.20",
|
||||
"expo": "^51.0.8",
|
||||
"expo-build-properties": "~0.12.1",
|
||||
"expo-constants": "~16.0.1",
|
||||
"expo-dev-client": "~4.0.14",
|
||||
"expo-file-system": "~17.0.1",
|
||||
"expo-font": "~12.0.5",
|
||||
"expo-image-picker": "~15.0.5",
|
||||
"expo-linear-gradient": "~13.0.2",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-localization": "~15.0.3",
|
||||
"expo-navigation-bar": "~3.0.4",
|
||||
"expo-router": "3.5.14",
|
||||
"expo-screen-orientation": "~7.0.5",
|
||||
"expo-secure-store": "~13.0.1",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"i18next": "^23.11.4",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
"moti": "^0.27.2",
|
||||
"moti": "^0.29.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^14.0.1",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-blurhash": "^1.1.11",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-blurhash": "^2.0.2",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-mmkv": "^2.11.0",
|
||||
"react-native-reanimated": "~3.6.2",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "~3.29.0",
|
||||
"react-native-svg": "14.1.0",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"react-native-video": "^6.0.0-beta.4",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.1",
|
||||
"react-native-screens": "~3.31.1",
|
||||
"react-native-svg": "15.2.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
"react-native-video": "^6.0.0",
|
||||
"yoshiki": "1.2.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@types/react": "18.2.48",
|
||||
"react-native-svg-transformer": "^1.3.0",
|
||||
"typescript": "^5.3.3"
|
||||
"@babel/core": "^7.24.5",
|
||||
"react-native-svg-transformer": "^1.4.0",
|
||||
"typescript": "~5.3.3"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
|
@ -16,46 +16,45 @@
|
||||
"@kyoo/models": "workspace:^",
|
||||
"@kyoo/primitives": "workspace:^",
|
||||
"@kyoo/ui": "workspace:^",
|
||||
"@material-symbols/svg-400": "^0.14.6",
|
||||
"@material-symbols/svg-400": "^0.18.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-query-devtools": "^5.17.21",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tanstack/react-query-devtools": "^5.37.1",
|
||||
"array-shuffle": "^3.0.0",
|
||||
"expo-image-picker": "~14.7.1",
|
||||
"expo-linear-gradient": "^12.7.1",
|
||||
"expo-modules-core": "^1.11.8",
|
||||
"hls.js": "^1.5.6",
|
||||
"i18next": "^23.7.20",
|
||||
"expo-image-picker": "~15.0.5",
|
||||
"expo-linear-gradient": "^13.0.2",
|
||||
"expo-modules-core": "^1.12.11",
|
||||
"hls.js": "^1.5.8",
|
||||
"i18next": "^23.11.4",
|
||||
"jassub": "^1.7.15",
|
||||
"jotai": "^2.6.3",
|
||||
"moti": "^0.27.2",
|
||||
"next": "14.1.0",
|
||||
"jotai": "^2.8.0",
|
||||
"moti": "^0.29.0",
|
||||
"next": "14.2.3",
|
||||
"next-translate": "^2.6.2",
|
||||
"raf": "^3.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^14.0.1",
|
||||
"react-native-reanimated": "3.6.2",
|
||||
"react-native-svg": "14.1.0",
|
||||
"react-native-video": "^6.0.0-beta.4",
|
||||
"react-native-web": "0.19.10",
|
||||
"react-tooltip": "^5.26.0",
|
||||
"solito": "^4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-native-reanimated": "3.11.0",
|
||||
"react-native-svg": "15.3.0",
|
||||
"react-native-video": "^6.0.0",
|
||||
"react-native-web": "0.19.11",
|
||||
"react-tooltip": "^5.26.4",
|
||||
"solito": "^4.2.2",
|
||||
"srt-webvtt": "zoriya/srt-webvtt#build",
|
||||
"superjson": "^2.2.1",
|
||||
"sweetalert2": "^11.10.4",
|
||||
"sweetalert2": "^11.11.0",
|
||||
"yoshiki": "1.2.14",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "20.11.7",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"react-native": "0.73.2",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.90.0"
|
||||
"react-native": "0.74.1",
|
||||
"typescript": "^5.4.5",
|
||||
"webpack": "^5.91.0"
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,7 @@ import i18next, { type InitOptions } from "i18next";
|
||||
import type { AppContext, AppInitialProps, AppProps } from "next/app";
|
||||
import { type ComponentType, useMemo } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import en from "../../../translations/en.json";
|
||||
import fr from "../../../translations/fr.json";
|
||||
import zh from "../../../translations/zh.json";
|
||||
import resources from "../../../translations";
|
||||
|
||||
export const withTranslations = (
|
||||
AppToTranslate: ComponentType<AppProps> & {
|
||||
@ -37,6 +34,7 @@ export const withTranslations = (
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources,
|
||||
};
|
||||
|
||||
const AppWithTranslations = (props: AppProps) => {
|
||||
@ -45,10 +43,10 @@ export const withTranslations = (
|
||||
i18next.init({
|
||||
...commonOptions,
|
||||
lng: props.pageProps.__lang,
|
||||
resources: props.pageProps.__resources,
|
||||
fallbackLng: "en",
|
||||
});
|
||||
return i18next;
|
||||
}, [props.pageProps.__lang, props.pageProps.__resources]);
|
||||
}, [props.pageProps.__lang]);
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={li18n}>
|
||||
@ -59,21 +57,12 @@ export const withTranslations = (
|
||||
AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
|
||||
const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx);
|
||||
const lng = ctx.router.locale || ctx.router.defaultLocale || "en";
|
||||
// TODO: use a backend to fetch only the needed translations.
|
||||
// TODO: use a different backend on the client and fetch needed translations.
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
fr: { translation: fr },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
await i18n.init({
|
||||
...commonOptions,
|
||||
lng,
|
||||
fallbackLng: ctx.router.defaultLocale || "en",
|
||||
resources,
|
||||
fallbackLng: "en",
|
||||
});
|
||||
props.pageProps.__lang = lng;
|
||||
props.pageProps.__resources = resources;
|
||||
return props;
|
||||
};
|
||||
|
||||
|
@ -17,7 +17,8 @@
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.7.3",
|
||||
"typescript": "5.3.3"
|
||||
"@types/react": "~18.2.79",
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"packageManager": "yarn@3.2.4"
|
||||
}
|
||||
|
@ -5,9 +5,8 @@
|
||||
"sideEffects": false,
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"devDependencies": {
|
||||
"@types/react": "18.2.48",
|
||||
"react-native-mmkv": "^2.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "*",
|
||||
@ -20,6 +19,6 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
|
@ -133,19 +133,31 @@ export const AccountProvider = ({
|
||||
setApiUrl(selected?.apiUrl ?? defaultApiUrl);
|
||||
}, [selected, setApiUrl]);
|
||||
|
||||
const user = useFetch({
|
||||
const {
|
||||
isSuccess: userIsSuccess,
|
||||
isError: userIsError,
|
||||
isLoading: userIsLoading,
|
||||
isPlaceholderData: userIsPlaceholder,
|
||||
data: user,
|
||||
error: userError,
|
||||
} = useFetch({
|
||||
path: ["auth", "me"],
|
||||
parser: UserP,
|
||||
placeholderData: selected as User,
|
||||
enabled: !!selected,
|
||||
});
|
||||
// Use a ref here because we don't want the effect to trigger when the selected
|
||||
// value has changed, only when the fetch result changed
|
||||
// If we trigger the effect when the selected value change, we enter an infinite render loop
|
||||
const selectedRef = useRef(selected);
|
||||
selectedRef.current = selected;
|
||||
useEffect(() => {
|
||||
if (!selected || !user.isSuccess || user.isPlaceholderData) return;
|
||||
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
|
||||
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
||||
if (user.data.id !== selected.id) return;
|
||||
const nUser = { ...selected, ...user.data };
|
||||
if (!Object.is(selected, nUser)) updateAccount(nUser.id, nUser);
|
||||
}, [selected, user]);
|
||||
if (user.id !== selectedRef.current.id) return;
|
||||
const nUser = { ...selectedRef.current, ...user };
|
||||
updateAccount(nUser.id, nUser);
|
||||
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const oldSelected = useRef<{ id: string; token: string } | null>(
|
||||
@ -154,7 +166,6 @@ export const AccountProvider = ({
|
||||
|
||||
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
|
||||
|
||||
const userIsError = user.isError;
|
||||
useEffect(() => {
|
||||
// if the user change account (or connect/disconnect), reset query cache.
|
||||
if (
|
||||
@ -180,8 +191,8 @@ export const AccountProvider = ({
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: (selected ? initialSsrError.current ?? user.error : null) ?? permissionError,
|
||||
loading: user.isLoading,
|
||||
error: (selected ? initialSsrError.current ?? userError : null) ?? permissionError,
|
||||
loading: userIsLoading,
|
||||
retry: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||
},
|
||||
|
@ -33,6 +33,12 @@ import { type Page, Paged } from "./page";
|
||||
|
||||
export let lastUsedUrl: string = null!;
|
||||
|
||||
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||
if (!str) return null;
|
||||
if (keepFirst) return str.replace(/\/$/g, "");
|
||||
return str.replace(/^\/|\/$/g, "");
|
||||
};
|
||||
|
||||
export const queryFn = async <Parser extends z.ZodTypeAny>(
|
||||
context: {
|
||||
apiUrl?: string | null;
|
||||
@ -54,17 +60,16 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
||||
lastUsedUrl = url!;
|
||||
|
||||
const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken;
|
||||
const path = [url]
|
||||
const path = [cleanSlash(url, true)]
|
||||
.concat(
|
||||
"path" in context
|
||||
? (context.path as string[])
|
||||
: "pageParam" in context && context.pageParam
|
||||
? [context.pageParam as string]
|
||||
? [cleanSlash(context.pageParam as string)]
|
||||
: (context.queryKey as string[]),
|
||||
)
|
||||
.filter((x) => x)
|
||||
.join("/")
|
||||
.replace("//", "/")
|
||||
.replace("/?", "?");
|
||||
let resp: Response;
|
||||
try {
|
||||
|
@ -18,21 +18,10 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import i18next from "i18next";
|
||||
import { z } from "zod";
|
||||
import { imageFn } from "../traits";
|
||||
import { QualityP } from "./quality";
|
||||
|
||||
const getDisplayName = (sub: Track) => {
|
||||
const languageNames = new Intl.DisplayNames([i18next.language ?? "en"], { type: "language" });
|
||||
const lng = sub.language ? languageNames.of(sub.language) : undefined;
|
||||
|
||||
if (lng && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`;
|
||||
if (lng) return lng;
|
||||
if (sub.title) return sub.title;
|
||||
return `Unknown (${sub.index})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Video track
|
||||
*/
|
||||
@ -97,10 +86,7 @@ export const TrackP = z.object({
|
||||
});
|
||||
export type Track = z.infer<typeof TrackP>;
|
||||
|
||||
export const AudioP = TrackP.transform((x) => ({
|
||||
...x,
|
||||
displayName: getDisplayName(x),
|
||||
}));
|
||||
export const AudioP = TrackP;
|
||||
export type Audio = z.infer<typeof AudioP>;
|
||||
|
||||
export const SubtitleP = TrackP.extend({
|
||||
@ -108,10 +94,7 @@ export const SubtitleP = TrackP.extend({
|
||||
* The url of this track (only if this is a subtitle)..
|
||||
*/
|
||||
link: z.string().transform(imageFn).nullable(),
|
||||
}).transform((x) => ({
|
||||
...x,
|
||||
displayName: getDisplayName(x),
|
||||
}));
|
||||
});
|
||||
export type Subtitle = z.infer<typeof SubtitleP>;
|
||||
|
||||
export const ChapterP = z.object({
|
||||
|
@ -6,8 +6,7 @@
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"devDependencies": {
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@types/react": "18.2.48",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@gorhom/portal": "*",
|
||||
@ -53,15 +52,15 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/html-elements": "^0.9.1",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"solito": "^4.2.0"
|
||||
"@expo/html-elements": "^0.10.1",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"solito": "^4.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"react-native-blurhash": "^1.1.11",
|
||||
"react-native-blurhash": "^2.0.2",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-safe-area-context": "4.8.2"
|
||||
"react-native-safe-area-context": "4.10.1"
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ const Menu = <AsProps,>({
|
||||
const [isOpen, setOpen] =
|
||||
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
||||
|
||||
// deos the same as a useMemo but for props.
|
||||
// does the same as a useMemo but for props.
|
||||
const memoRef = useRef({ onMenuOpen, onMenuClose });
|
||||
memoRef.current = { onMenuOpen, onMenuClose };
|
||||
useEffect(() => {
|
||||
|
@ -18,7 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import { Button } from "./button";
|
||||
import { Icon } from "./icons";
|
||||
import { Menu } from "./menu";
|
||||
|
@ -19,8 +19,8 @@
|
||||
*/
|
||||
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||
import * as RSelect from "@radix-ui/react-select";
|
||||
import { forwardRef } from "react";
|
||||
import { View } from "react-native";
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import type { Property } from "csstype";
|
||||
import type { ReactNode } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { Platform, type TextStyle } from "react-native";
|
||||
import { type Theme, ThemeProvider, useAutomaticTheme } from "yoshiki";
|
||||
import "yoshiki";
|
||||
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||
@ -28,10 +28,7 @@ import "yoshiki/native";
|
||||
import { catppuccin } from "./catppuccin";
|
||||
|
||||
type FontList = Partial<
|
||||
Record<
|
||||
"normal" | "bold" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900",
|
||||
string
|
||||
>
|
||||
Record<Exclude<TextStyle["fontWeight"], null | undefined | number>, string>
|
||||
>;
|
||||
|
||||
type Mode = {
|
||||
@ -150,7 +147,7 @@ export const ThemeSelector = ({
|
||||
}) => {
|
||||
const newTheme = selectMode({ ...catppuccin, font }, theme);
|
||||
|
||||
return <ThemeProvider theme={newTheme}>{children}</ThemeProvider>;
|
||||
return <ThemeProvider theme={newTheme}>{children as any}</ThemeProvider>;
|
||||
};
|
||||
|
||||
export type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
|
||||
@ -165,7 +162,11 @@ export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc<
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={switchVariant(theme)}>
|
||||
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
||||
{typeof children === "function" ? (
|
||||
<YoshikiProvider>{children}</YoshikiProvider>
|
||||
) : (
|
||||
(children as any)
|
||||
)}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
@ -197,7 +198,11 @@ export const ContrastArea = ({
|
||||
: theme
|
||||
}
|
||||
>
|
||||
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
||||
{typeof children === "function" ? (
|
||||
<YoshikiProvider>{children}</YoshikiProvider>
|
||||
) : (
|
||||
(children as any)
|
||||
)}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
@ -5,14 +5,15 @@
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"dependencies": {
|
||||
"@kyoo/models": "workspace:^",
|
||||
"@kyoo/primitives": "workspace:^"
|
||||
"@kyoo/primitives": "workspace:^",
|
||||
"langmap": "^0.0.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@shopify/flash-list": "^1.6.3",
|
||||
"@types/react": "18.2.48",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"@shopify/flash-list": "^1.6.4",
|
||||
"@types/langmap": "^0.0.3",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@gorhom/portal": "*",
|
||||
@ -34,9 +35,9 @@
|
||||
"yoshiki": "*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@kesha-antonov/react-native-background-downloader": "git+https://github.com/zoriya/react-native-background-downloader.git",
|
||||
"expo-file-system": "^16.0.5",
|
||||
"expo-router": "^3.4.6"
|
||||
"@kesha-antonov/react-native-background-downloader": "^3.1.3",
|
||||
"expo-file-system": "^17.0.1",
|
||||
"expo-router": "^3.5.14"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@kesha-antonov/react-native-background-downloader": {
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
important,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
||||
import Done from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { type ImageStyle, Platform, View } from "react-native";
|
||||
import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
|
@ -95,7 +95,7 @@ export const EpisodesContext = ({
|
||||
{Object.values(WatchStatusV).map((x) => (
|
||||
<Menu.Item
|
||||
key={x}
|
||||
label={t(`show.watchlistMark.${x.toLowerCase()}`)}
|
||||
label={t(`show.watchlistMark.${x.toLowerCase() as Lowercase<WatchStatusV>}`)}
|
||||
onSelect={() => mutation.mutate(x)}
|
||||
selected={x === status}
|
||||
/>
|
||||
|
@ -31,16 +31,19 @@ import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Fetch } from "../fetch";
|
||||
import { useDisplayName } from "../utils";
|
||||
|
||||
const MediaInfoTable = ({
|
||||
mediaInfo: { path, video, container, audios, subtitles, duration, size },
|
||||
}: {
|
||||
mediaInfo: Partial<WatchInfo>;
|
||||
}) => {
|
||||
const getDisplayName = useDisplayName();
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`;
|
||||
const formatTrackTable = (trackTable: (Audio | Subtitle)[], s: string) => {
|
||||
const formatTrackTable = (trackTable: (Audio | Subtitle)[], type: "subtitles" | "audio") => {
|
||||
if (trackTable.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
@ -48,15 +51,16 @@ const MediaInfoTable = ({
|
||||
return trackTable.reduce(
|
||||
(collected, audioTrack, index) => {
|
||||
// If there is only one track, we do not need to show an index
|
||||
collected[singleTrack ? t(s) : `${t(s)} ${index + 1}`] = [
|
||||
audioTrack.displayName,
|
||||
// Only show it if there is more than one track
|
||||
audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
|
||||
audioTrack.isForced ? t("mediainfo.forced") : undefined,
|
||||
audioTrack.codec,
|
||||
]
|
||||
.filter((x) => x !== undefined)
|
||||
.join(" - ");
|
||||
collected[singleTrack ? t(`mediainfo.${type}`) : `${t(`mediainfo.${type}`)} ${index + 1}`] =
|
||||
[
|
||||
getDisplayName(audioTrack),
|
||||
// Only show it if there is more than one track
|
||||
audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
|
||||
audioTrack.isForced ? t("mediainfo.forced") : undefined,
|
||||
audioTrack.codec,
|
||||
]
|
||||
.filter((x) => x !== undefined)
|
||||
.join(" - ");
|
||||
return collected;
|
||||
},
|
||||
{} as Record<string, string | undefined>,
|
||||
@ -81,10 +85,10 @@ const MediaInfoTable = ({
|
||||
},
|
||||
audios === undefined
|
||||
? { [t("mediainfo.audio")]: undefined }
|
||||
: formatTrackTable(audios, "mediainfo.audio"),
|
||||
: formatTrackTable(audios, "audio"),
|
||||
subtitles === undefined
|
||||
? { [t("mediainfo.subtitles")]: undefined }
|
||||
: formatTrackTable(subtitles, "mediainfo.subtitles"),
|
||||
: formatTrackTable(subtitles, "subtitles"),
|
||||
] as const
|
||||
).filter((x) => x !== undefined) as Record<string, string | undefined>[];
|
||||
return (
|
||||
|
@ -33,8 +33,8 @@ import {
|
||||
tooltip,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type ImageStyle, Platform, type PressableProps, View } from "react-native";
|
||||
|
@ -36,15 +36,17 @@ import {
|
||||
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
|
||||
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||
import { deleteAsync } from "expo-file-system";
|
||||
import type { Router } from "expo-router/build/types";
|
||||
import type { useRouter } from "expo-router";
|
||||
import { type PrimitiveAtom, atom, useSetAtom, useStore } from "jotai";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { ToastAndroid } from "react-native";
|
||||
import { z } from "zod";
|
||||
import { Player } from "../player";
|
||||
|
||||
type Router = ReturnType<typeof useRouter>;
|
||||
|
||||
export type State = {
|
||||
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
|
||||
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED" | "PENDING";
|
||||
progress: number | null;
|
||||
size: number;
|
||||
availableSize: number;
|
||||
@ -190,7 +192,7 @@ const download = (
|
||||
headers: {
|
||||
Authorization: account.token.access_token,
|
||||
},
|
||||
showNotification: true,
|
||||
isNotificationVisible: true,
|
||||
// TODO: Implement only wifi
|
||||
// network: Network.ALL,
|
||||
});
|
||||
|
@ -24,6 +24,6 @@ import type en from "../../../translations/en.json";
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
resources: { translations: typeof en };
|
||||
resources: { translation: typeof en };
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* 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 "i18next";
|
||||
import type en from "../../../translations/en.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
resources: { translations: typeof en };
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ const query: QueryIdentifier<ServerInfo> = {
|
||||
export const ServerUrlPage: QueryPage = () => {
|
||||
const [_apiUrl, setApiUrl] = useState("");
|
||||
const apiUrl = cleanApiUrl(_apiUrl);
|
||||
const { data, error } = useFetch({ ...query, options: { apiUrl } });
|
||||
const { data, error } = useFetch({ ...query, options: { apiUrl, authenticated: false } });
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
@ -295,6 +295,7 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
playerWidth.current = e.nativeEvent.layout.width;
|
||||
}}
|
||||
{...css(
|
||||
// @ts-expect-error Web only property (cursor: unset)
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
@ -304,7 +305,6 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
// @ts-expect-error Web only property
|
||||
cursor: hover ? "unset" : "none",
|
||||
},
|
||||
props,
|
||||
|
@ -29,6 +29,7 @@ import { useAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { type Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { useDisplayName } from "../../utils";
|
||||
import { fullscreenAtom, subtitleAtom } from "../state";
|
||||
import { AudiosMenu, QualitiesMenu } from "../video";
|
||||
|
||||
@ -48,6 +49,7 @@ export const RightButtons = ({
|
||||
} & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const getDisplayName = useDisplayName();
|
||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||
const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom);
|
||||
|
||||
@ -72,7 +74,7 @@ export const RightButtons = ({
|
||||
{subtitles.map((x) => (
|
||||
<Menu.Item
|
||||
key={x.index}
|
||||
label={x.link ? x.displayName : `${x.displayName} (${x.codec})`}
|
||||
label={x.link ? getDisplayName(x) : `${getDisplayName(x)} (${x.codec})`}
|
||||
selected={selectedSubtitle === x}
|
||||
disabled={!x.link}
|
||||
onSelect={() => setSubtitle(x)}
|
||||
|
@ -39,7 +39,6 @@ import { episodeDisplayNumber } from "../details/episode";
|
||||
import { ErrorView } from "../errors";
|
||||
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
||||
import { useVideoKeyboard } from "./keyboard";
|
||||
import { MediaSessionManager } from "./media-session";
|
||||
import { Video, durationAtom, fullscreenAtom } from "./state";
|
||||
import { WatchStatusObserver } from "./watch-status-observer";
|
||||
|
||||
@ -90,6 +89,15 @@ export const Player = ({
|
||||
data && data.type === "episode" && data.nextEpisode
|
||||
? `/watch/${data.nextEpisode.slug}?t=0`
|
||||
: undefined;
|
||||
const title =
|
||||
data &&
|
||||
(data.type === "movie"
|
||||
? data.name
|
||||
: `${data.show!.name} ${episodeDisplayNumber({
|
||||
seasonNumber: data.seasonNumber,
|
||||
episodeNumber: data.episodeNumber,
|
||||
absoluteNumber: data.absoluteNumber,
|
||||
})}`);
|
||||
|
||||
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||
|
||||
@ -119,26 +127,7 @@ export const Player = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{data && (
|
||||
<Head
|
||||
title={
|
||||
data.type === "movie"
|
||||
? data.name
|
||||
: `${data.show!.name} ${episodeDisplayNumber({
|
||||
seasonNumber: data.seasonNumber,
|
||||
episodeNumber: data.episodeNumber,
|
||||
absoluteNumber: data.absoluteNumber,
|
||||
})}`
|
||||
}
|
||||
description={data.overview}
|
||||
/>
|
||||
)}
|
||||
<MediaSessionManager
|
||||
title={data?.name ?? t("show.episodeNoMetadata")}
|
||||
image={data?.thumbnail?.high}
|
||||
next={next}
|
||||
previous={previous}
|
||||
/>
|
||||
<Head title={title} description={data?.overview} />
|
||||
{data && info && (
|
||||
<WatchStatusObserver type={type} slug={data.slug} duration={info.durationSeconds} />
|
||||
)}
|
||||
@ -150,6 +139,13 @@ export const Player = ({
|
||||
})}
|
||||
>
|
||||
<Video
|
||||
metadata={{
|
||||
title: title ?? t("show.episodeNoMetadata"),
|
||||
description: data?.overview ?? undefined,
|
||||
imageUri: data?.thumbnail?.high,
|
||||
next: next,
|
||||
previous: previous,
|
||||
}}
|
||||
links={data?.links}
|
||||
audios={info?.audios}
|
||||
subtitles={info?.subtitles}
|
||||
|
@ -104,6 +104,7 @@ export const Video = memo(function Video({
|
||||
setError,
|
||||
fonts,
|
||||
startTime: startTimeP,
|
||||
metadata,
|
||||
...props
|
||||
}: {
|
||||
links?: Episode["links"];
|
||||
@ -113,6 +114,13 @@ export const Video = memo(function Video({
|
||||
setError: (error: string | undefined) => void;
|
||||
fonts?: string[];
|
||||
startTime?: number | null;
|
||||
metadata: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
imageUri?: string;
|
||||
previous?: string;
|
||||
next?: string;
|
||||
};
|
||||
} & Partial<VideoProps>) {
|
||||
const ref = useRef<ElementRef<typeof NativeVideo> | null>(null);
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
@ -220,8 +228,12 @@ export const Video = memo(function Video({
|
||||
source={{
|
||||
uri: source,
|
||||
startPosition: startTime.current ? startTime.current * 1000 : undefined,
|
||||
metadata: metadata,
|
||||
...links,
|
||||
}}
|
||||
showNotificationControls
|
||||
playInBackground
|
||||
playWhenInactive
|
||||
paused={!isPlaying}
|
||||
muted={isMuted}
|
||||
volume={volume}
|
||||
|
@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
import "react-native-video";
|
||||
import type { ReactVideoSourceProperties } from "react-native-video";
|
||||
|
||||
declare module "react-native-video" {
|
||||
interface ReactVideoProps {
|
||||
@ -27,7 +28,7 @@ declare module "react-native-video" {
|
||||
onMediaUnsupported?: () => void;
|
||||
}
|
||||
export type VideoProps = Omit<ReactVideoProps, "source"> & {
|
||||
source: { uri: string; hls: string | null; startPosition?: number };
|
||||
source: ReactVideoSourceProperties & { hls: string | null };
|
||||
};
|
||||
}
|
||||
|
||||
@ -49,6 +50,7 @@ import NativeVideo, {
|
||||
SelectedVideoTrackType,
|
||||
} from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { useDisplayName } from "../utils";
|
||||
import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./state";
|
||||
|
||||
const MimeTypes: Map<string, string> = new Map([
|
||||
@ -102,7 +104,7 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
||||
}}
|
||||
selectedVideoTrack={
|
||||
video === -1
|
||||
? { type: SelectedVideoTrackType.AUDO }
|
||||
? { type: SelectedVideoTrackType.AUTO }
|
||||
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
|
||||
}
|
||||
// when video file is invalid, audio is undefined
|
||||
@ -119,7 +121,7 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: subtitles?.indexOf(subtitle),
|
||||
}
|
||||
: { type: SelectedTrackType.DISABLED }
|
||||
: { type: SelectedTrackType.DISABLED, value: "" }
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
@ -130,12 +132,13 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
||||
export default Video;
|
||||
|
||||
// mobile should be able to play everything
|
||||
export const canPlay = (codec: string) => true;
|
||||
export const canPlay = (_codec: string) => true;
|
||||
|
||||
type CustomMenu = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||
export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => {
|
||||
const info = useAtomValue(infoAtom);
|
||||
const [audio, setAudio] = useAtom(audioAtom);
|
||||
const getDisplayName = useDisplayName();
|
||||
|
||||
if (!info || info.audioTracks.length < 2) return null;
|
||||
|
||||
@ -144,7 +147,7 @@ export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[]
|
||||
{info.audioTracks.map((x) => (
|
||||
<Menu.Item
|
||||
key={x.index}
|
||||
label={audios?.[x.index].displayName ?? x.title ?? x.language ?? "Unknown"}
|
||||
label={audios ? getDisplayName(audios[x.index]) : x.title ?? x.language ?? "Unknown"}
|
||||
selected={audio!.index === x.index}
|
||||
onSelect={() => setAudio(x as any)}
|
||||
/>
|
||||
|
@ -36,6 +36,8 @@ import { useTranslation } from "react-i18next";
|
||||
import type { VideoProps } from "react-native-video";
|
||||
import toVttBlob from "srt-webvtt";
|
||||
import { useForceRerender, useYoshiki } from "yoshiki";
|
||||
import { useDisplayName } from "../utils";
|
||||
import { MediaSessionManager } from "./media-session";
|
||||
import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state";
|
||||
|
||||
let hls: Hls | null = null;
|
||||
@ -207,46 +209,49 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
||||
const setProgress = useSetAtom(progressAtom);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={ref}
|
||||
src={source.uri}
|
||||
muted={muted}
|
||||
autoPlay={!paused}
|
||||
controls={false}
|
||||
playsInline
|
||||
onCanPlay={() => onBuffer?.call(null, { isBuffering: false })}
|
||||
onWaiting={() => onBuffer?.call(null, { isBuffering: true })}
|
||||
onDurationChange={() => {
|
||||
if (!ref.current) return;
|
||||
onLoad?.call(null, { duration: ref.current.duration } as any);
|
||||
}}
|
||||
onTimeUpdate={() => {
|
||||
if (!ref.current) return;
|
||||
onProgress?.call(null, {
|
||||
currentTime: ref.current.currentTime,
|
||||
playableDuration: ref.current.buffered.length
|
||||
? ref.current.buffered.end(ref.current.buffered.length - 1)
|
||||
: 0,
|
||||
seekableDuration: 0,
|
||||
});
|
||||
}}
|
||||
onError={() => {
|
||||
if (ref?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
||||
onMediaUnsupported?.call(undefined);
|
||||
else {
|
||||
onError?.call(null, {
|
||||
error: { errorString: ref.current?.error?.message ?? "Unknown error" },
|
||||
<>
|
||||
<MediaSessionManager {...source.metadata} />
|
||||
<video
|
||||
ref={ref}
|
||||
src={source.uri}
|
||||
muted={muted}
|
||||
autoPlay={!paused}
|
||||
controls={false}
|
||||
playsInline
|
||||
onCanPlay={() => onBuffer?.call(null, { isBuffering: false })}
|
||||
onWaiting={() => onBuffer?.call(null, { isBuffering: true })}
|
||||
onDurationChange={() => {
|
||||
if (!ref.current) return;
|
||||
onLoad?.call(null, { duration: ref.current.duration } as any);
|
||||
}}
|
||||
onTimeUpdate={() => {
|
||||
if (!ref.current) return;
|
||||
onProgress?.call(null, {
|
||||
currentTime: ref.current.currentTime,
|
||||
playableDuration: ref.current.buffered.length
|
||||
? ref.current.buffered.end(ref.current.buffered.length - 1)
|
||||
: 0,
|
||||
seekableDuration: 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLoadedMetadata={() => {
|
||||
if (source.startPosition) setProgress(source.startPosition / 1000);
|
||||
}}
|
||||
onPlay={() => onPlaybackStateChanged?.({ isPlaying: true })}
|
||||
onPause={() => onPlaybackStateChanged?.({ isPlaying: false })}
|
||||
onEnded={onEnd}
|
||||
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
||||
/>
|
||||
}}
|
||||
onError={() => {
|
||||
if (ref?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
||||
onMediaUnsupported?.call(undefined);
|
||||
else {
|
||||
onError?.call(null, {
|
||||
error: { errorString: ref.current?.error?.message ?? "Unknown error" },
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLoadedMetadata={() => {
|
||||
if (source.startPosition) setProgress(source.startPosition / 1000);
|
||||
}}
|
||||
onPlay={() => onPlaybackStateChanged?.({ isPlaying: true })}
|
||||
onPause={() => onPlaybackStateChanged?.({ isPlaying: false })}
|
||||
onEnded={onEnd}
|
||||
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@ -292,7 +297,7 @@ const useSubtitle = (
|
||||
const addSubtitle = async () => {
|
||||
const track: HTMLTrackElement = htmlTrack.current ?? document.createElement("track");
|
||||
track.kind = "subtitles";
|
||||
track.label = value.displayName;
|
||||
track.label = value.title ?? value.language ?? "Subtitle";
|
||||
if (value.language) track.srclang = value.language;
|
||||
track.src = value.codec === "subrip" ? await toWebVtt(value.link!) : value.link!;
|
||||
track.className = "subtitle_container";
|
||||
@ -360,6 +365,7 @@ export const AudiosMenu = ({
|
||||
const { t } = useTranslation();
|
||||
const rerender = useForceRerender();
|
||||
const [_, setAudio] = useAtom(audioAtom);
|
||||
const getDisplayName = useDisplayName();
|
||||
// force rerender when mode changes
|
||||
useAtomValue(playModeAtom);
|
||||
|
||||
@ -377,7 +383,7 @@ export const AudiosMenu = ({
|
||||
{hls.audioTracks.map((x, i) => (
|
||||
<Menu.Item
|
||||
key={i.toString()}
|
||||
label={audios?.[i]?.displayName ?? x.name}
|
||||
label={audios ? getDisplayName(audios[i]) : x.name}
|
||||
selected={hls!.audioTrack === i}
|
||||
onSelect={() => setAudio(audios?.[i] ?? ({ index: i } as any))}
|
||||
/>
|
||||
|
@ -28,14 +28,12 @@ import Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||
|
||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||
import { useLanguageName } from "../utils";
|
||||
|
||||
export const GeneralSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const theme = useUserTheme("auto");
|
||||
const languages = new Intl.DisplayNames([i18n.language ?? "en"], {
|
||||
type: "language",
|
||||
languageDisplay: "standard",
|
||||
});
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
@ -65,7 +63,7 @@ export const GeneralSettings = () => {
|
||||
}
|
||||
values={["system", ...Object.keys(i18n.options.resources!)]}
|
||||
getLabel={(key) =>
|
||||
key === "system" ? t("settings.general.language.system") : languages.of(key) ?? key
|
||||
key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
|
@ -20,30 +20,23 @@
|
||||
|
||||
import { useLocalSetting } from "@kyoo/models";
|
||||
import { Select } from "@kyoo/primitives";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlayMode, playModeAtom } from "../player/state";
|
||||
import { Preference, SettingsContainer, useSetting } from "./base";
|
||||
|
||||
import SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
||||
import PlayModeI from "@material-symbols/svg-400/rounded/display_settings-fill.svg";
|
||||
import AudioLanguage from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
||||
import intl from "langmap";
|
||||
import { useLanguageName } from "../utils";
|
||||
|
||||
// I gave up on finding a way to retrive this using the Intl api (probably does not exist)
|
||||
// Simply copy pasted the list of languages from https://www.localeplanet.com/api/codelist.json
|
||||
// biome-ignore format: way too long
|
||||
const allLanguages = ["af", "agq", "ak", "am", "ar", "as", "asa", "ast", "az", "bas", "be", "bem", "bez", "bg", "bm", "bn", "bo", "br", "brx", "bs", "ca", "ccp", "ce", "cgg", "chr", "ckb", "cs", "cy", "da", "dav", "de", "dje", "dsb", "dua", "dyo", "dz", "ebu", "ee", "el", "en", "eo", "es", "et", "eu", "ewo", "fa", "ff", "fi", "fil", "fo", "fr", "fur", "fy", "ga", "gd", "gl", "gsw", "gu", "guz", "gv", "ha", "haw", "he", "hi", "hr", "hsb", "hu", "hy", "id", "ig", "ii", "is", "it", "ja", "jgo", "jmc", "ka", "kab", "kam", "kde", "kea", "khq", "ki", "kk", "kkj", "kl", "kln", "km", "kn", "ko", "kok", "ks", "ksb", "ksf", "ksh", "kw", "ky", "lag", "lb", "lg", "lkt", "ln", "lo", "lrc", "lt", "lu", "luo", "luy", "lv", "mas", "mer", "mfe", "mg", "mgh", "mgo", "mk", "ml", "mn", "mr", "ms", "mt", "mua", "my", "mzn", "naq", "nb", "nd", "nds", "ne", "nl", "nmg", "nn", "nnh", "nus", "nyn", "om", "or", "os", "pa", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "rof", "ru", "rw", "rwk", "sah", "saq", "sbp", "se", "seh", "ses", "sg", "shi", "si", "sk", "sl", "smn", "sn", "so", "sq", "sr", "sv", "sw", "ta", "te", "teo", "tg", "th", "ti", "to", "tr", "tt", "twq", "tzm", "ug", "uk", "ur", "uz", "vai", "vi", "vun", "wae", "wo", "xog", "yav", "yi", "yo", "yue", "zgh", "zh", "zu",];
|
||||
const allLanguages = Object.keys(intl).filter((x) => !x.includes("-") && !x.includes("@"));
|
||||
|
||||
export const PlaybackSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [playMode, setDefaultPlayMode] = useLocalSetting("playmode", "direct");
|
||||
const setCurrentPlayMode = useSetAtom(playModeAtom);
|
||||
const [audio, setAudio] = useSetting("audioLanguage")!;
|
||||
const [subtitle, setSubtitle] = useSetting("subtitleLanguage")!;
|
||||
const languages = new Intl.DisplayNames([i18n.language ?? "en"], {
|
||||
type: "language",
|
||||
languageDisplay: "standard",
|
||||
});
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.playback.label")}>
|
||||
@ -57,7 +50,7 @@ export const PlaybackSettings = () => {
|
||||
value={playMode}
|
||||
onValueChange={(value) => setDefaultPlayMode(value)}
|
||||
values={["direct", "auto"]}
|
||||
getLabel={(key) => t(`player.${key}`)}
|
||||
getLabel={(key) => t(`player.${key}` as any)}
|
||||
/>
|
||||
</Preference>
|
||||
<Preference
|
||||
@ -71,7 +64,7 @@ export const PlaybackSettings = () => {
|
||||
onValueChange={(value) => setAudio(value)}
|
||||
values={["default", ...allLanguages]}
|
||||
getLabel={(key) =>
|
||||
key === "default" ? t("mediainfo.default") : languages.of(key) ?? key
|
||||
key === "default" ? t("mediainfo.default") : getLanguageName(key) ?? key
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
@ -90,7 +83,7 @@ export const PlaybackSettings = () => {
|
||||
? t("settings.playback.subtitleLanguage.none")
|
||||
: key === "default"
|
||||
? t("mediainfo.default")
|
||||
: languages.of(key) ?? key
|
||||
: getLanguageName(key) ?? key
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
|
19
front/packages/ui/src/utils.ts
Normal file
19
front/packages/ui/src/utils.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Track } from "@kyoo/models";
|
||||
import intl from "langmap";
|
||||
|
||||
export const useLanguageName = () => {
|
||||
return (lang: string) => intl[lang]?.nativeName;
|
||||
};
|
||||
|
||||
export const useDisplayName = () => {
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (sub: Track) => {
|
||||
const lng = sub.language ? getLanguageName(sub.language) : null;
|
||||
|
||||
if (lng && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`;
|
||||
if (lng) return lng;
|
||||
if (sub.title) return sub.title;
|
||||
return `Unknown (${sub.index})`;
|
||||
};
|
||||
};
|
17
front/translations/index.js
Normal file
17
front/translations/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import de from "./de";
|
||||
import en from "./en";
|
||||
import fr from "./fr";
|
||||
import pl from "./pl";
|
||||
import tr from "./tr";
|
||||
import uk from "./uk";
|
||||
import zh from "./zh";
|
||||
|
||||
export default {
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
fr: { translation: fr },
|
||||
pl: { translation: pl },
|
||||
tr: { translation: tr },
|
||||
uk: { translation: uk },
|
||||
zh: { translation: zh },
|
||||
};
|
@ -1,180 +1,28 @@
|
||||
{
|
||||
"home": {
|
||||
"recommended": "Polecane",
|
||||
"news": "",
|
||||
"watchlist": "Kontynuuj oglądanie",
|
||||
"info": "Zobacz więcej",
|
||||
"none": "Brak odcinków",
|
||||
"watchlistLogin": "Żeby śledzić co było oglądane albo co planujesz oglądać musisz się zalogować.",
|
||||
"refreshMetadata": "Odśwież metadane",
|
||||
"episodeMore": {
|
||||
"goToShow": "",
|
||||
"download": "",
|
||||
"mediainfo": ""
|
||||
}
|
||||
"refreshMetadata": "Odśwież metadane"
|
||||
},
|
||||
"show": {
|
||||
"play": "Odtwórz",
|
||||
"trailer": "Odtwórz zwiastun",
|
||||
"studio": "",
|
||||
"genre": "",
|
||||
"genre-none": "",
|
||||
"staff": "",
|
||||
"staff-none": "",
|
||||
"noOverview": "",
|
||||
"episode-none": "Brak odcinków w tym sezonie",
|
||||
"episodeNoMetadata": "",
|
||||
"tags": "",
|
||||
"links": "",
|
||||
"jumpToSeason": "",
|
||||
"partOf": "",
|
||||
"watchlistAdd": "",
|
||||
"watchlistEdit": "",
|
||||
"watchlistRemove": "",
|
||||
"watchlistLogin": "",
|
||||
"watchlistMark": {
|
||||
"completed": "",
|
||||
"planned": "",
|
||||
"watching": "",
|
||||
"droped": "",
|
||||
"null": ""
|
||||
},
|
||||
"nextUp": ""
|
||||
},
|
||||
"browse": {
|
||||
"sortby": "",
|
||||
"sortby-tt": "",
|
||||
"sortkey": {
|
||||
"relevance": "",
|
||||
"name": "",
|
||||
"airDate": "",
|
||||
"startAir": "",
|
||||
"endAir": "",
|
||||
"addedDate": "",
|
||||
"rating": ""
|
||||
},
|
||||
"sortord": {
|
||||
"asc": "",
|
||||
"desc": ""
|
||||
},
|
||||
"switchToGrid": "",
|
||||
"switchToList": ""
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
"prev-page": "",
|
||||
"next-page": "",
|
||||
"delete": "",
|
||||
"cancel": "",
|
||||
"more": "",
|
||||
"expand": "",
|
||||
"collapse": "",
|
||||
"edit": "",
|
||||
"or": "",
|
||||
"loading": ""
|
||||
},
|
||||
"navbar": {
|
||||
"home": "",
|
||||
"browse": "",
|
||||
"search": "",
|
||||
"login": "",
|
||||
"admin": ""
|
||||
"episode-none": "Brak odcinków w tym sezonie"
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"label": "",
|
||||
"theme": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"auto": "",
|
||||
"light": "",
|
||||
"dark": ""
|
||||
},
|
||||
"language": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"system": ""
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
"label": "",
|
||||
"playmode": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
},
|
||||
"audioLanguage": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
},
|
||||
"subtitleLanguage": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"none": ""
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"label": "",
|
||||
"username": {
|
||||
"label": ""
|
||||
},
|
||||
"avatar": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
},
|
||||
"email": {
|
||||
"label": ""
|
||||
},
|
||||
"password": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"oldPassword": "",
|
||||
"newPassword": ""
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"label": "",
|
||||
"connected": "",
|
||||
"not-connected": "",
|
||||
"open-profile": "",
|
||||
"link": "",
|
||||
"delete": ""
|
||||
},
|
||||
"about": {
|
||||
"label": "",
|
||||
"android-app": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
},
|
||||
"git": {
|
||||
"label": "",
|
||||
"description": "Otwórz repozytorium github gdzie możesz przejrzeć kod kyoo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"back": "",
|
||||
"previous": "",
|
||||
"next": "",
|
||||
"play": "",
|
||||
"pause": "",
|
||||
"mute": "",
|
||||
"volume": "",
|
||||
"quality": "",
|
||||
"audios": "",
|
||||
"subtitles": "",
|
||||
"subtitle-none": "",
|
||||
"fullscreen": "Pełny ekran",
|
||||
"direct": "",
|
||||
"transmux": "",
|
||||
"auto": "",
|
||||
"notInPristine": "",
|
||||
"unsupportedError": ""
|
||||
},
|
||||
"search": {
|
||||
"empty": ""
|
||||
"fullscreen": "Pełny ekran"
|
||||
},
|
||||
"login": {
|
||||
"login": "",
|
||||
"register": "Zarejestruj",
|
||||
"guest": "Kontynuuj jako gość",
|
||||
"guest-forbidden": "Ta instancja kyoo nie zezwala na korzystanie jako gość",
|
||||
@ -186,59 +34,11 @@
|
||||
"username": "Nazwa użytkownika",
|
||||
"password": "Hasło",
|
||||
"confirm": "Powtórz hasło",
|
||||
"or-register": "",
|
||||
"or-login": "",
|
||||
"password-no-match": "",
|
||||
"delete": "Usuń konto",
|
||||
"delete-confirmation": ""
|
||||
},
|
||||
"downloads": {
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"delete": "",
|
||||
"deleteMessage": "",
|
||||
"pause": "",
|
||||
"resume": "",
|
||||
"retry": ""
|
||||
"delete": "Usuń konto"
|
||||
},
|
||||
"errors": {
|
||||
"connection": "",
|
||||
"connection-tips": "",
|
||||
"unknown": "Nieznany błąd",
|
||||
"try-again": "Spróbuj ponownie",
|
||||
"re-login": "Zaloguj się ponownie",
|
||||
"offline": "",
|
||||
"unauthorized": "",
|
||||
"needVerification": "",
|
||||
"needAccount": ""
|
||||
},
|
||||
"mediainfo": {
|
||||
"file": "",
|
||||
"container": "",
|
||||
"video": "",
|
||||
"audio": "",
|
||||
"subtitles": "",
|
||||
"forced": "",
|
||||
"default": "",
|
||||
"duration": "",
|
||||
"size": "",
|
||||
"novideo": "",
|
||||
"nocontainer": ""
|
||||
},
|
||||
"admin": {
|
||||
"users": {
|
||||
"label": "",
|
||||
"adminUser": "",
|
||||
"regularUser": "",
|
||||
"set-permissions": "",
|
||||
"delete": "",
|
||||
"unverifed": "",
|
||||
"verify": ""
|
||||
},
|
||||
"scanner": {
|
||||
"label": "",
|
||||
"scan": "",
|
||||
"empty": ""
|
||||
}
|
||||
"re-login": "Zaloguj się ponownie"
|
||||
}
|
||||
}
|
||||
|
2658
front/yarn.lock
2658
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user