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,
|
Poppins_900Black,
|
||||||
useFonts,
|
useFonts,
|
||||||
} from "@expo-google-fonts/poppins";
|
} 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 { PortalProvider } from "@gorhom/portal";
|
||||||
import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models";
|
import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models";
|
||||||
import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives";
|
import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives";
|
||||||
@ -45,14 +41,11 @@ import "intl-pluralrules";
|
|||||||
import { type ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
|
import resources from "../../../translations";
|
||||||
|
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import { onlineManager } from "@tanstack/react-query";
|
import { onlineManager } from "@tanstack/react-query";
|
||||||
import { useTheme } from "yoshiki/native";
|
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) => {
|
onlineManager.setEventListener((setOnline) => {
|
||||||
return NetInfo.addEventListener((state) => {
|
return NetInfo.addEventListener((state) => {
|
||||||
@ -81,11 +74,7 @@ i18next.use(initReactI18next).init({
|
|||||||
},
|
},
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
lng: getLocales()[0].languageCode ?? "en",
|
lng: getLocales()[0].languageCode ?? "en",
|
||||||
resources: {
|
resources,
|
||||||
en: { translation: en },
|
|
||||||
fr: { translation: fr },
|
|
||||||
zh: { translation: zh },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
|
const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
@ -15,58 +15,57 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/poppins": "^0.2.3",
|
"@expo-google-fonts/poppins": "^0.2.3",
|
||||||
"@formatjs/intl-displaynames": "^6.6.6",
|
"@formatjs/intl-displaynames": "^6.6.8",
|
||||||
"@formatjs/intl-locale": "^3.4.5",
|
"@formatjs/intl-locale": "^4.0.0",
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@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:^",
|
"@kyoo/ui": "workspace:^",
|
||||||
"@material-symbols/svg-400": "^0.14.6",
|
"@material-symbols/svg-400": "^0.18.0",
|
||||||
"@react-native-community/netinfo": "11.1.0",
|
"@react-native-community/netinfo": "11.3.1",
|
||||||
"@shopify/flash-list": "1.6.3",
|
"@shopify/flash-list": "1.6.4",
|
||||||
"@tanstack/query-sync-storage-persister": "^5.17.19",
|
"@tanstack/query-sync-storage-persister": "^5.37.1",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.37.1",
|
||||||
"@tanstack/react-query-persist-client": "^5.17.19",
|
"@tanstack/react-query-persist-client": "^5.37.1",
|
||||||
"array-shuffle": "^3.0.0",
|
"array-shuffle": "^3.0.0",
|
||||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||||
"expo": "^50.0.4",
|
"expo": "^51.0.8",
|
||||||
"expo-build-properties": "~0.11.0",
|
"expo-build-properties": "~0.12.1",
|
||||||
"expo-constants": "~15.4.5",
|
"expo-constants": "~16.0.1",
|
||||||
"expo-dev-client": "~3.3.7",
|
"expo-dev-client": "~4.0.14",
|
||||||
"expo-file-system": "~16.0.5",
|
"expo-file-system": "~17.0.1",
|
||||||
"expo-font": "~11.10.2",
|
"expo-font": "~12.0.5",
|
||||||
"expo-image-picker": "~14.7.1",
|
"expo-image-picker": "~15.0.5",
|
||||||
"expo-linear-gradient": "~12.7.1",
|
"expo-linear-gradient": "~13.0.2",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-localization": "~14.8.3",
|
"expo-localization": "~15.0.3",
|
||||||
"expo-navigation-bar": "~2.8.1",
|
"expo-navigation-bar": "~3.0.4",
|
||||||
"expo-router": "3.4.6",
|
"expo-router": "3.5.14",
|
||||||
"expo-screen-orientation": "~6.4.1",
|
"expo-screen-orientation": "~7.0.5",
|
||||||
"expo-secure-store": "~12.8.1",
|
"expo-secure-store": "~13.0.1",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-updates": "~0.24.9",
|
"expo-updates": "~0.25.14",
|
||||||
"i18next": "^23.7.20",
|
"i18next": "^23.11.4",
|
||||||
"intl-pluralrules": "^2.0.1",
|
"intl-pluralrules": "^2.0.1",
|
||||||
"moti": "^0.27.2",
|
"moti": "^0.29.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-i18next": "^14.0.1",
|
"react-i18next": "^14.1.1",
|
||||||
"react-native": "0.73.2",
|
"react-native": "0.74.1",
|
||||||
"react-native-blurhash": "^1.1.11",
|
"react-native-blurhash": "^2.0.2",
|
||||||
"react-native-fast-image": "^8.6.3",
|
"react-native-fast-image": "^8.6.3",
|
||||||
"react-native-mmkv": "^2.11.0",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-reanimated": "~3.6.2",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.10.1",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.31.1",
|
||||||
"react-native-svg": "14.1.0",
|
"react-native-svg": "15.2.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.2",
|
||||||
"react-native-video": "^6.0.0-beta.4",
|
"react-native-video": "^6.0.0",
|
||||||
"yoshiki": "1.2.14"
|
"yoshiki": "1.2.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.23.9",
|
"@babel/core": "^7.24.5",
|
||||||
"@types/react": "18.2.48",
|
"react-native-svg-transformer": "^1.4.0",
|
||||||
"react-native-svg-transformer": "^1.3.0",
|
"typescript": "~5.3.3"
|
||||||
"typescript": "^5.3.3"
|
|
||||||
},
|
},
|
||||||
"installConfig": {
|
"installConfig": {
|
||||||
"hoistingLimits": "workspaces"
|
"hoistingLimits": "workspaces"
|
||||||
|
@ -16,46 +16,45 @@
|
|||||||
"@kyoo/models": "workspace:^",
|
"@kyoo/models": "workspace:^",
|
||||||
"@kyoo/primitives": "workspace:^",
|
"@kyoo/primitives": "workspace:^",
|
||||||
"@kyoo/ui": "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-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.37.1",
|
||||||
"@tanstack/react-query-devtools": "^5.17.21",
|
"@tanstack/react-query-devtools": "^5.37.1",
|
||||||
"array-shuffle": "^3.0.0",
|
"array-shuffle": "^3.0.0",
|
||||||
"expo-image-picker": "~14.7.1",
|
"expo-image-picker": "~15.0.5",
|
||||||
"expo-linear-gradient": "^12.7.1",
|
"expo-linear-gradient": "^13.0.2",
|
||||||
"expo-modules-core": "^1.11.8",
|
"expo-modules-core": "^1.12.11",
|
||||||
"hls.js": "^1.5.6",
|
"hls.js": "^1.5.8",
|
||||||
"i18next": "^23.7.20",
|
"i18next": "^23.11.4",
|
||||||
"jassub": "^1.7.15",
|
"jassub": "^1.7.15",
|
||||||
"jotai": "^2.6.3",
|
"jotai": "^2.8.0",
|
||||||
"moti": "^0.27.2",
|
"moti": "^0.29.0",
|
||||||
"next": "14.1.0",
|
"next": "14.2.3",
|
||||||
"next-translate": "^2.6.2",
|
"next-translate": "^2.6.2",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^14.0.1",
|
"react-i18next": "^14.1.1",
|
||||||
"react-native-reanimated": "3.6.2",
|
"react-native-reanimated": "3.11.0",
|
||||||
"react-native-svg": "14.1.0",
|
"react-native-svg": "15.3.0",
|
||||||
"react-native-video": "^6.0.0-beta.4",
|
"react-native-video": "^6.0.0",
|
||||||
"react-native-web": "0.19.10",
|
"react-native-web": "0.19.11",
|
||||||
"react-tooltip": "^5.26.0",
|
"react-tooltip": "^5.26.4",
|
||||||
"solito": "^4.2.0",
|
"solito": "^4.2.2",
|
||||||
"srt-webvtt": "zoriya/srt-webvtt#build",
|
"srt-webvtt": "zoriya/srt-webvtt#build",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"sweetalert2": "^11.10.4",
|
"sweetalert2": "^11.11.0",
|
||||||
"yoshiki": "1.2.14",
|
"yoshiki": "1.2.14",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/node": "20.11.7",
|
"@types/node": "20.12.12",
|
||||||
"@types/react": "18.2.48",
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/react-dom": "18.2.18",
|
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"react-native": "0.73.2",
|
"react-native": "0.74.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.5",
|
||||||
"webpack": "^5.90.0"
|
"webpack": "^5.91.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,7 @@ import i18next, { type InitOptions } from "i18next";
|
|||||||
import type { AppContext, AppInitialProps, AppProps } from "next/app";
|
import type { AppContext, AppInitialProps, AppProps } from "next/app";
|
||||||
import { type ComponentType, useMemo } from "react";
|
import { type ComponentType, useMemo } from "react";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
import resources from "../../../translations";
|
||||||
import en from "../../../translations/en.json";
|
|
||||||
import fr from "../../../translations/fr.json";
|
|
||||||
import zh from "../../../translations/zh.json";
|
|
||||||
|
|
||||||
export const withTranslations = (
|
export const withTranslations = (
|
||||||
AppToTranslate: ComponentType<AppProps> & {
|
AppToTranslate: ComponentType<AppProps> & {
|
||||||
@ -37,6 +34,7 @@ export const withTranslations = (
|
|||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
|
resources,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppWithTranslations = (props: AppProps) => {
|
const AppWithTranslations = (props: AppProps) => {
|
||||||
@ -45,10 +43,10 @@ export const withTranslations = (
|
|||||||
i18next.init({
|
i18next.init({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
lng: props.pageProps.__lang,
|
lng: props.pageProps.__lang,
|
||||||
resources: props.pageProps.__resources,
|
fallbackLng: "en",
|
||||||
});
|
});
|
||||||
return i18next;
|
return i18next;
|
||||||
}, [props.pageProps.__lang, props.pageProps.__resources]);
|
}, [props.pageProps.__lang]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={li18n}>
|
<I18nextProvider i18n={li18n}>
|
||||||
@ -59,21 +57,12 @@ export const withTranslations = (
|
|||||||
AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
|
AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
|
||||||
const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx);
|
const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx);
|
||||||
const lng = ctx.router.locale || ctx.router.defaultLocale || "en";
|
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({
|
await i18n.init({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
lng,
|
lng,
|
||||||
fallbackLng: ctx.router.defaultLocale || "en",
|
fallbackLng: "en",
|
||||||
resources,
|
|
||||||
});
|
});
|
||||||
props.pageProps.__lang = lng;
|
props.pageProps.__lang = lng;
|
||||||
props.pageProps.__resources = resources;
|
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
"workspaces": ["apps/*", "packages/*"],
|
"workspaces": ["apps/*", "packages/*"],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.7.3",
|
"@biomejs/biome": "1.7.3",
|
||||||
"typescript": "5.3.3"
|
"@types/react": "~18.2.79",
|
||||||
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.4"
|
"packageManager": "yarn@3.2.4"
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,8 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"packageManager": "yarn@3.2.4",
|
"packageManager": "yarn@3.2.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "18.2.48",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-mmkv": "^2.11.0",
|
"typescript": "^5.4.5"
|
||||||
"typescript": "^5.3.3"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tanstack/react-query": "*",
|
"@tanstack/react-query": "*",
|
||||||
@ -20,6 +19,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,19 +133,31 @@ export const AccountProvider = ({
|
|||||||
setApiUrl(selected?.apiUrl ?? defaultApiUrl);
|
setApiUrl(selected?.apiUrl ?? defaultApiUrl);
|
||||||
}, [selected, setApiUrl]);
|
}, [selected, setApiUrl]);
|
||||||
|
|
||||||
const user = useFetch({
|
const {
|
||||||
|
isSuccess: userIsSuccess,
|
||||||
|
isError: userIsError,
|
||||||
|
isLoading: userIsLoading,
|
||||||
|
isPlaceholderData: userIsPlaceholder,
|
||||||
|
data: user,
|
||||||
|
error: userError,
|
||||||
|
} = useFetch({
|
||||||
path: ["auth", "me"],
|
path: ["auth", "me"],
|
||||||
parser: UserP,
|
parser: UserP,
|
||||||
placeholderData: selected as User,
|
placeholderData: selected as User,
|
||||||
enabled: !!selected,
|
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(() => {
|
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.
|
// 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;
|
if (user.id !== selectedRef.current.id) return;
|
||||||
const nUser = { ...selected, ...user.data };
|
const nUser = { ...selectedRef.current, ...user };
|
||||||
if (!Object.is(selected, nUser)) updateAccount(nUser.id, nUser);
|
updateAccount(nUser.id, nUser);
|
||||||
}, [selected, user]);
|
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const oldSelected = useRef<{ id: string; token: string } | null>(
|
const oldSelected = useRef<{ id: string; token: string } | null>(
|
||||||
@ -154,7 +166,6 @@ export const AccountProvider = ({
|
|||||||
|
|
||||||
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
|
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
|
||||||
|
|
||||||
const userIsError = user.isError;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if the user change account (or connect/disconnect), reset query cache.
|
// if the user change account (or connect/disconnect), reset query cache.
|
||||||
if (
|
if (
|
||||||
@ -180,8 +191,8 @@ export const AccountProvider = ({
|
|||||||
<AccountContext.Provider value={accounts}>
|
<AccountContext.Provider value={accounts}>
|
||||||
<ConnectionErrorContext.Provider
|
<ConnectionErrorContext.Provider
|
||||||
value={{
|
value={{
|
||||||
error: (selected ? initialSsrError.current ?? user.error : null) ?? permissionError,
|
error: (selected ? initialSsrError.current ?? userError : null) ?? permissionError,
|
||||||
loading: user.isLoading,
|
loading: userIsLoading,
|
||||||
retry: () => {
|
retry: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||||
},
|
},
|
||||||
|
@ -33,6 +33,12 @@ import { type Page, Paged } from "./page";
|
|||||||
|
|
||||||
export let lastUsedUrl: string = null!;
|
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>(
|
export const queryFn = async <Parser extends z.ZodTypeAny>(
|
||||||
context: {
|
context: {
|
||||||
apiUrl?: string | null;
|
apiUrl?: string | null;
|
||||||
@ -54,17 +60,16 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
lastUsedUrl = url!;
|
lastUsedUrl = url!;
|
||||||
|
|
||||||
const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken;
|
const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken;
|
||||||
const path = [url]
|
const path = [cleanSlash(url, true)]
|
||||||
.concat(
|
.concat(
|
||||||
"path" in context
|
"path" in context
|
||||||
? (context.path as string[])
|
? (context.path as string[])
|
||||||
: "pageParam" in context && context.pageParam
|
: "pageParam" in context && context.pageParam
|
||||||
? [context.pageParam as string]
|
? [cleanSlash(context.pageParam as string)]
|
||||||
: (context.queryKey as string[]),
|
: (context.queryKey as string[]),
|
||||||
)
|
)
|
||||||
.filter((x) => x)
|
.filter((x) => x)
|
||||||
.join("/")
|
.join("/")
|
||||||
.replace("//", "/")
|
|
||||||
.replace("/?", "?");
|
.replace("/?", "?");
|
||||||
let resp: Response;
|
let resp: Response;
|
||||||
try {
|
try {
|
||||||
|
@ -18,21 +18,10 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import i18next from "i18next";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { imageFn } from "../traits";
|
import { imageFn } from "../traits";
|
||||||
import { QualityP } from "./quality";
|
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
|
* A Video track
|
||||||
*/
|
*/
|
||||||
@ -97,10 +86,7 @@ export const TrackP = z.object({
|
|||||||
});
|
});
|
||||||
export type Track = z.infer<typeof TrackP>;
|
export type Track = z.infer<typeof TrackP>;
|
||||||
|
|
||||||
export const AudioP = TrackP.transform((x) => ({
|
export const AudioP = TrackP;
|
||||||
...x,
|
|
||||||
displayName: getDisplayName(x),
|
|
||||||
}));
|
|
||||||
export type Audio = z.infer<typeof AudioP>;
|
export type Audio = z.infer<typeof AudioP>;
|
||||||
|
|
||||||
export const SubtitleP = TrackP.extend({
|
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)..
|
* The url of this track (only if this is a subtitle)..
|
||||||
*/
|
*/
|
||||||
link: z.string().transform(imageFn).nullable(),
|
link: z.string().transform(imageFn).nullable(),
|
||||||
}).transform((x) => ({
|
});
|
||||||
...x,
|
|
||||||
displayName: getDisplayName(x),
|
|
||||||
}));
|
|
||||||
export type Subtitle = z.infer<typeof SubtitleP>;
|
export type Subtitle = z.infer<typeof SubtitleP>;
|
||||||
|
|
||||||
export const ChapterP = z.object({
|
export const ChapterP = z.object({
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
"packageManager": "yarn@3.2.4",
|
"packageManager": "yarn@3.2.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@gorhom/portal": "^1.0.14",
|
||||||
"@types/react": "18.2.48",
|
"typescript": "^5.4.5"
|
||||||
"typescript": "^5.3.3"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@gorhom/portal": "*",
|
"@gorhom/portal": "*",
|
||||||
@ -53,15 +52,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/html-elements": "^0.9.1",
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.37.1",
|
||||||
"solito": "^4.2.0"
|
"solito": "^4.2.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"blurhash": "^2.0.5",
|
"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-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] =
|
const [isOpen, setOpen] =
|
||||||
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
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 });
|
const memoRef = useRef({ onMenuOpen, onMenuClose });
|
||||||
memoRef.current = { onMenuOpen, onMenuClose };
|
memoRef.current = { onMenuOpen, onMenuClose };
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { Button } from "./button";
|
||||||
import { Icon } from "./icons";
|
import { Icon } from "./icons";
|
||||||
import { Menu } from "./menu";
|
import { Menu } from "./menu";
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
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/keyboard_arrow_down-fill.svg";
|
||||||
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||||
import * as RSelect from "@radix-ui/react-select";
|
import * as RSelect from "@radix-ui/react-select";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import type { Property } from "csstype";
|
import type { Property } from "csstype";
|
||||||
import type { ReactNode } from "react";
|
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 { type Theme, ThemeProvider, useAutomaticTheme } from "yoshiki";
|
||||||
import "yoshiki";
|
import "yoshiki";
|
||||||
import { useTheme, useYoshiki } from "yoshiki/native";
|
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||||
@ -28,10 +28,7 @@ import "yoshiki/native";
|
|||||||
import { catppuccin } from "./catppuccin";
|
import { catppuccin } from "./catppuccin";
|
||||||
|
|
||||||
type FontList = Partial<
|
type FontList = Partial<
|
||||||
Record<
|
Record<Exclude<TextStyle["fontWeight"], null | undefined | number>, string>
|
||||||
"normal" | "bold" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900",
|
|
||||||
string
|
|
||||||
>
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Mode = {
|
type Mode = {
|
||||||
@ -150,7 +147,7 @@ export const ThemeSelector = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const newTheme = selectMode({ ...catppuccin, font }, theme);
|
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;
|
export type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
|
||||||
@ -165,7 +162,11 @@ export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={switchVariant(theme)}>
|
<ThemeProvider theme={switchVariant(theme)}>
|
||||||
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
{typeof children === "function" ? (
|
||||||
|
<YoshikiProvider>{children}</YoshikiProvider>
|
||||||
|
) : (
|
||||||
|
(children as any)
|
||||||
|
)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -197,7 +198,11 @@ export const ContrastArea = ({
|
|||||||
: theme
|
: theme
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{typeof children === "function" ? <YoshikiProvider>{children}</YoshikiProvider> : children}
|
{typeof children === "function" ? (
|
||||||
|
<YoshikiProvider>{children}</YoshikiProvider>
|
||||||
|
) : (
|
||||||
|
(children as any)
|
||||||
|
)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,14 +5,15 @@
|
|||||||
"packageManager": "yarn@3.2.4",
|
"packageManager": "yarn@3.2.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kyoo/models": "workspace:^",
|
"@kyoo/models": "workspace:^",
|
||||||
"@kyoo/primitives": "workspace:^"
|
"@kyoo/primitives": "workspace:^",
|
||||||
|
"langmap": "^0.0.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@gorhom/portal": "^1.0.14",
|
||||||
"@shopify/flash-list": "^1.6.3",
|
"@shopify/flash-list": "^1.6.4",
|
||||||
"@types/react": "18.2.48",
|
"@types/langmap": "^0.0.3",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@gorhom/portal": "*",
|
"@gorhom/portal": "*",
|
||||||
@ -34,9 +35,9 @@
|
|||||||
"yoshiki": "*"
|
"yoshiki": "*"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@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",
|
||||||
"expo-file-system": "^16.0.5",
|
"expo-file-system": "^17.0.1",
|
||||||
"expo-router": "^3.4.6"
|
"expo-router": "^3.5.14"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@kesha-antonov/react-native-background-downloader": {
|
"@kesha-antonov/react-native-background-downloader": {
|
||||||
|
@ -30,7 +30,7 @@ import {
|
|||||||
important,
|
important,
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} 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 { useState } from "react";
|
||||||
import { type ImageStyle, Platform, View } from "react-native";
|
import { type ImageStyle, Platform, View } from "react-native";
|
||||||
import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/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) => (
|
{Object.values(WatchStatusV).map((x) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={x}
|
key={x}
|
||||||
label={t(`show.watchlistMark.${x.toLowerCase()}`)}
|
label={t(`show.watchlistMark.${x.toLowerCase() as Lowercase<WatchStatusV>}`)}
|
||||||
onSelect={() => mutation.mutate(x)}
|
onSelect={() => mutation.mutate(x)}
|
||||||
selected={x === status}
|
selected={x === status}
|
||||||
/>
|
/>
|
||||||
|
@ -31,16 +31,19 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { Fetch } from "../fetch";
|
import { Fetch } from "../fetch";
|
||||||
|
import { useDisplayName } from "../utils";
|
||||||
|
|
||||||
const MediaInfoTable = ({
|
const MediaInfoTable = ({
|
||||||
mediaInfo: { path, video, container, audios, subtitles, duration, size },
|
mediaInfo: { path, video, container, audios, subtitles, duration, size },
|
||||||
}: {
|
}: {
|
||||||
mediaInfo: Partial<WatchInfo>;
|
mediaInfo: Partial<WatchInfo>;
|
||||||
}) => {
|
}) => {
|
||||||
|
const getDisplayName = useDisplayName();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`;
|
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) {
|
if (trackTable.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -48,8 +51,9 @@ const MediaInfoTable = ({
|
|||||||
return trackTable.reduce(
|
return trackTable.reduce(
|
||||||
(collected, audioTrack, index) => {
|
(collected, audioTrack, index) => {
|
||||||
// If there is only one track, we do not need to show an index
|
// If there is only one track, we do not need to show an index
|
||||||
collected[singleTrack ? t(s) : `${t(s)} ${index + 1}`] = [
|
collected[singleTrack ? t(`mediainfo.${type}`) : `${t(`mediainfo.${type}`)} ${index + 1}`] =
|
||||||
audioTrack.displayName,
|
[
|
||||||
|
getDisplayName(audioTrack),
|
||||||
// Only show it if there is more than one track
|
// Only show it if there is more than one track
|
||||||
audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
|
audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
|
||||||
audioTrack.isForced ? t("mediainfo.forced") : undefined,
|
audioTrack.isForced ? t("mediainfo.forced") : undefined,
|
||||||
@ -81,10 +85,10 @@ const MediaInfoTable = ({
|
|||||||
},
|
},
|
||||||
audios === undefined
|
audios === undefined
|
||||||
? { [t("mediainfo.audio")]: undefined }
|
? { [t("mediainfo.audio")]: undefined }
|
||||||
: formatTrackTable(audios, "mediainfo.audio"),
|
: formatTrackTable(audios, "audio"),
|
||||||
subtitles === undefined
|
subtitles === undefined
|
||||||
? { [t("mediainfo.subtitles")]: undefined }
|
? { [t("mediainfo.subtitles")]: undefined }
|
||||||
: formatTrackTable(subtitles, "mediainfo.subtitles"),
|
: formatTrackTable(subtitles, "subtitles"),
|
||||||
] as const
|
] as const
|
||||||
).filter((x) => x !== undefined) as Record<string, string | undefined>[];
|
).filter((x) => x !== undefined) as Record<string, string | undefined>[];
|
||||||
return (
|
return (
|
||||||
|
@ -33,8 +33,8 @@ import {
|
|||||||
tooltip,
|
tooltip,
|
||||||
ts,
|
ts,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
|
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||||
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
|
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type ImageStyle, Platform, type PressableProps, View } from "react-native";
|
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 { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
|
||||||
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||||
import { deleteAsync } from "expo-file-system";
|
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 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 "../player";
|
||||||
|
|
||||||
|
type Router = ReturnType<typeof useRouter>;
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
|
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED" | "PENDING";
|
||||||
progress: number | null;
|
progress: number | null;
|
||||||
size: number;
|
size: number;
|
||||||
availableSize: number;
|
availableSize: number;
|
||||||
@ -190,7 +192,7 @@ const download = (
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: account.token.access_token,
|
Authorization: account.token.access_token,
|
||||||
},
|
},
|
||||||
showNotification: true,
|
isNotificationVisible: true,
|
||||||
// TODO: Implement only wifi
|
// TODO: Implement only wifi
|
||||||
// network: Network.ALL,
|
// network: Network.ALL,
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,6 @@ import type en from "../../../translations/en.json";
|
|||||||
declare module "i18next" {
|
declare module "i18next" {
|
||||||
interface CustomTypeOptions {
|
interface CustomTypeOptions {
|
||||||
returnNull: false;
|
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 = () => {
|
export const ServerUrlPage: QueryPage = () => {
|
||||||
const [_apiUrl, setApiUrl] = useState("");
|
const [_apiUrl, setApiUrl] = useState("");
|
||||||
const apiUrl = cleanApiUrl(_apiUrl);
|
const apiUrl = cleanApiUrl(_apiUrl);
|
||||||
const { data, error } = useFetch({ ...query, options: { apiUrl } });
|
const { data, error } = useFetch({ ...query, options: { apiUrl, authenticated: false } });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
@ -295,6 +295,7 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
|||||||
playerWidth.current = e.nativeEvent.layout.width;
|
playerWidth.current = e.nativeEvent.layout.width;
|
||||||
}}
|
}}
|
||||||
{...css(
|
{...css(
|
||||||
|
// @ts-expect-error Web only property (cursor: unset)
|
||||||
{
|
{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@ -304,7 +305,6 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
// @ts-expect-error Web only property
|
|
||||||
cursor: hover ? "unset" : "none",
|
cursor: hover ? "unset" : "none",
|
||||||
},
|
},
|
||||||
props,
|
props,
|
||||||
|
@ -29,6 +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 { useDisplayName } from "../../utils";
|
||||||
import { fullscreenAtom, subtitleAtom } from "../state";
|
import { fullscreenAtom, subtitleAtom } from "../state";
|
||||||
import { AudiosMenu, QualitiesMenu } from "../video";
|
import { AudiosMenu, QualitiesMenu } from "../video";
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ export const RightButtons = ({
|
|||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const getDisplayName = useDisplayName();
|
||||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||||
const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom);
|
const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom);
|
||||||
|
|
||||||
@ -72,7 +74,7 @@ export const RightButtons = ({
|
|||||||
{subtitles.map((x) => (
|
{subtitles.map((x) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={x.index}
|
key={x.index}
|
||||||
label={x.link ? x.displayName : `${x.displayName} (${x.codec})`}
|
label={x.link ? getDisplayName(x) : `${getDisplayName(x)} (${x.codec})`}
|
||||||
selected={selectedSubtitle === x}
|
selected={selectedSubtitle === x}
|
||||||
disabled={!x.link}
|
disabled={!x.link}
|
||||||
onSelect={() => setSubtitle(x)}
|
onSelect={() => setSubtitle(x)}
|
||||||
|
@ -39,7 +39,6 @@ import { episodeDisplayNumber } from "../details/episode";
|
|||||||
import { ErrorView } from "../errors";
|
import { ErrorView } from "../errors";
|
||||||
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
import { Back, Hover, LoadingIndicator } from "./components/hover";
|
||||||
import { useVideoKeyboard } from "./keyboard";
|
import { useVideoKeyboard } from "./keyboard";
|
||||||
import { MediaSessionManager } from "./media-session";
|
|
||||||
import { Video, durationAtom, fullscreenAtom } from "./state";
|
import { Video, durationAtom, fullscreenAtom } from "./state";
|
||||||
import { WatchStatusObserver } from "./watch-status-observer";
|
import { WatchStatusObserver } from "./watch-status-observer";
|
||||||
|
|
||||||
@ -90,6 +89,15 @@ export const Player = ({
|
|||||||
data && data.type === "episode" && data.nextEpisode
|
data && data.type === "episode" && data.nextEpisode
|
||||||
? `/watch/${data.nextEpisode.slug}?t=0`
|
? `/watch/${data.nextEpisode.slug}?t=0`
|
||||||
: undefined;
|
: 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);
|
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||||
|
|
||||||
@ -119,26 +127,7 @@ export const Player = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && (
|
<Head title={title} description={data?.overview} />
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
{data && info && (
|
{data && info && (
|
||||||
<WatchStatusObserver type={type} slug={data.slug} duration={info.durationSeconds} />
|
<WatchStatusObserver type={type} slug={data.slug} duration={info.durationSeconds} />
|
||||||
)}
|
)}
|
||||||
@ -150,6 +139,13 @@ export const Player = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Video
|
<Video
|
||||||
|
metadata={{
|
||||||
|
title: title ?? t("show.episodeNoMetadata"),
|
||||||
|
description: data?.overview ?? undefined,
|
||||||
|
imageUri: data?.thumbnail?.high,
|
||||||
|
next: next,
|
||||||
|
previous: previous,
|
||||||
|
}}
|
||||||
links={data?.links}
|
links={data?.links}
|
||||||
audios={info?.audios}
|
audios={info?.audios}
|
||||||
subtitles={info?.subtitles}
|
subtitles={info?.subtitles}
|
||||||
|
@ -104,6 +104,7 @@ export const Video = memo(function Video({
|
|||||||
setError,
|
setError,
|
||||||
fonts,
|
fonts,
|
||||||
startTime: startTimeP,
|
startTime: startTimeP,
|
||||||
|
metadata,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
links?: Episode["links"];
|
links?: Episode["links"];
|
||||||
@ -113,6 +114,13 @@ export const Video = memo(function Video({
|
|||||||
setError: (error: string | undefined) => void;
|
setError: (error: string | undefined) => void;
|
||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
startTime?: number | null;
|
startTime?: number | null;
|
||||||
|
metadata: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
imageUri?: string;
|
||||||
|
previous?: string;
|
||||||
|
next?: string;
|
||||||
|
};
|
||||||
} & Partial<VideoProps>) {
|
} & Partial<VideoProps>) {
|
||||||
const ref = useRef<ElementRef<typeof NativeVideo> | null>(null);
|
const ref = useRef<ElementRef<typeof NativeVideo> | null>(null);
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||||
@ -220,8 +228,12 @@ export const Video = memo(function Video({
|
|||||||
source={{
|
source={{
|
||||||
uri: source,
|
uri: source,
|
||||||
startPosition: startTime.current ? startTime.current * 1000 : undefined,
|
startPosition: startTime.current ? startTime.current * 1000 : undefined,
|
||||||
|
metadata: metadata,
|
||||||
...links,
|
...links,
|
||||||
}}
|
}}
|
||||||
|
showNotificationControls
|
||||||
|
playInBackground
|
||||||
|
playWhenInactive
|
||||||
paused={!isPlaying}
|
paused={!isPlaying}
|
||||||
muted={isMuted}
|
muted={isMuted}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "react-native-video";
|
import "react-native-video";
|
||||||
|
import type { ReactVideoSourceProperties } from "react-native-video";
|
||||||
|
|
||||||
declare module "react-native-video" {
|
declare module "react-native-video" {
|
||||||
interface ReactVideoProps {
|
interface ReactVideoProps {
|
||||||
@ -27,7 +28,7 @@ declare module "react-native-video" {
|
|||||||
onMediaUnsupported?: () => void;
|
onMediaUnsupported?: () => void;
|
||||||
}
|
}
|
||||||
export type VideoProps = Omit<ReactVideoProps, "source"> & {
|
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,
|
SelectedVideoTrackType,
|
||||||
} from "react-native-video";
|
} from "react-native-video";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
|
import { useDisplayName } from "../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([
|
||||||
@ -102,7 +104,7 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
|||||||
}}
|
}}
|
||||||
selectedVideoTrack={
|
selectedVideoTrack={
|
||||||
video === -1
|
video === -1
|
||||||
? { type: SelectedVideoTrackType.AUDO }
|
? { type: SelectedVideoTrackType.AUTO }
|
||||||
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
|
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
|
||||||
}
|
}
|
||||||
// when video file is invalid, audio is undefined
|
// when video file is invalid, audio is undefined
|
||||||
@ -119,7 +121,7 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
|||||||
type: SelectedTrackType.INDEX,
|
type: SelectedTrackType.INDEX,
|
||||||
value: subtitles?.indexOf(subtitle),
|
value: subtitles?.indexOf(subtitle),
|
||||||
}
|
}
|
||||||
: { type: SelectedTrackType.DISABLED }
|
: { type: SelectedTrackType.DISABLED, value: "" }
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -130,12 +132,13 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
|||||||
export default Video;
|
export default Video;
|
||||||
|
|
||||||
// mobile should be able to play everything
|
// 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>>>;
|
type CustomMenu = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||||
export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => {
|
export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => {
|
||||||
const info = useAtomValue(infoAtom);
|
const info = useAtomValue(infoAtom);
|
||||||
const [audio, setAudio] = useAtom(audioAtom);
|
const [audio, setAudio] = useAtom(audioAtom);
|
||||||
|
const getDisplayName = useDisplayName();
|
||||||
|
|
||||||
if (!info || info.audioTracks.length < 2) return null;
|
if (!info || info.audioTracks.length < 2) return null;
|
||||||
|
|
||||||
@ -144,7 +147,7 @@ export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[]
|
|||||||
{info.audioTracks.map((x) => (
|
{info.audioTracks.map((x) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={x.index}
|
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}
|
selected={audio!.index === x.index}
|
||||||
onSelect={() => setAudio(x as any)}
|
onSelect={() => setAudio(x as any)}
|
||||||
/>
|
/>
|
||||||
|
@ -36,6 +36,8 @@ 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 { MediaSessionManager } from "./media-session";
|
||||||
import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state";
|
import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state";
|
||||||
|
|
||||||
let hls: Hls | null = null;
|
let hls: Hls | null = null;
|
||||||
@ -207,6 +209,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
const setProgress = useSetAtom(progressAtom);
|
const setProgress = useSetAtom(progressAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<MediaSessionManager {...source.metadata} />
|
||||||
<video
|
<video
|
||||||
ref={ref}
|
ref={ref}
|
||||||
src={source.uri}
|
src={source.uri}
|
||||||
@ -247,6 +251,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
onEnded={onEnd}
|
onEnded={onEnd}
|
||||||
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,7 +297,7 @@ const useSubtitle = (
|
|||||||
const addSubtitle = async () => {
|
const addSubtitle = async () => {
|
||||||
const track: HTMLTrackElement = htmlTrack.current ?? document.createElement("track");
|
const track: HTMLTrackElement = htmlTrack.current ?? document.createElement("track");
|
||||||
track.kind = "subtitles";
|
track.kind = "subtitles";
|
||||||
track.label = value.displayName;
|
track.label = value.title ?? value.language ?? "Subtitle";
|
||||||
if (value.language) track.srclang = value.language;
|
if (value.language) track.srclang = value.language;
|
||||||
track.src = value.codec === "subrip" ? await toWebVtt(value.link!) : value.link!;
|
track.src = value.codec === "subrip" ? await toWebVtt(value.link!) : value.link!;
|
||||||
track.className = "subtitle_container";
|
track.className = "subtitle_container";
|
||||||
@ -360,6 +365,7 @@ export const AudiosMenu = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const rerender = useForceRerender();
|
const rerender = useForceRerender();
|
||||||
const [_, setAudio] = useAtom(audioAtom);
|
const [_, setAudio] = useAtom(audioAtom);
|
||||||
|
const getDisplayName = useDisplayName();
|
||||||
// force rerender when mode changes
|
// force rerender when mode changes
|
||||||
useAtomValue(playModeAtom);
|
useAtomValue(playModeAtom);
|
||||||
|
|
||||||
@ -377,7 +383,7 @@ export const AudiosMenu = ({
|
|||||||
{hls.audioTracks.map((x, i) => (
|
{hls.audioTracks.map((x, i) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={i.toString()}
|
key={i.toString()}
|
||||||
label={audios?.[i]?.displayName ?? x.name}
|
label={audios ? getDisplayName(audios[i]) : x.name}
|
||||||
selected={hls!.audioTrack === i}
|
selected={hls!.audioTrack === i}
|
||||||
onSelect={() => setAudio(audios?.[i] ?? ({ index: i } as any))}
|
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 Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||||
|
import { useLanguageName } from "../utils";
|
||||||
|
|
||||||
export const GeneralSettings = () => {
|
export const GeneralSettings = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const theme = useUserTheme("auto");
|
const theme = useUserTheme("auto");
|
||||||
const languages = new Intl.DisplayNames([i18n.language ?? "en"], {
|
const getLanguageName = useLanguageName();
|
||||||
type: "language",
|
|
||||||
languageDisplay: "standard",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer title={t("settings.general.label")}>
|
<SettingsContainer title={t("settings.general.label")}>
|
||||||
@ -65,7 +63,7 @@ export const GeneralSettings = () => {
|
|||||||
}
|
}
|
||||||
values={["system", ...Object.keys(i18n.options.resources!)]}
|
values={["system", ...Object.keys(i18n.options.resources!)]}
|
||||||
getLabel={(key) =>
|
getLabel={(key) =>
|
||||||
key === "system" ? t("settings.general.language.system") : languages.of(key) ?? key
|
key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
|
@ -20,30 +20,23 @@
|
|||||||
|
|
||||||
import { useLocalSetting } from "@kyoo/models";
|
import { useLocalSetting } from "@kyoo/models";
|
||||||
import { Select } from "@kyoo/primitives";
|
import { Select } from "@kyoo/primitives";
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PlayMode, playModeAtom } from "../player/state";
|
|
||||||
import { Preference, SettingsContainer, useSetting } from "./base";
|
import { Preference, SettingsContainer, useSetting } from "./base";
|
||||||
|
|
||||||
import SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
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 PlayModeI from "@material-symbols/svg-400/rounded/display_settings-fill.svg";
|
||||||
import AudioLanguage from "@material-symbols/svg-400/rounded/music_note-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)
|
const allLanguages = Object.keys(intl).filter((x) => !x.includes("-") && !x.includes("@"));
|
||||||
// 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",];
|
|
||||||
|
|
||||||
export const PlaybackSettings = () => {
|
export const PlaybackSettings = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [playMode, setDefaultPlayMode] = useLocalSetting("playmode", "direct");
|
const [playMode, setDefaultPlayMode] = useLocalSetting("playmode", "direct");
|
||||||
const setCurrentPlayMode = useSetAtom(playModeAtom);
|
|
||||||
const [audio, setAudio] = useSetting("audioLanguage")!;
|
const [audio, setAudio] = useSetting("audioLanguage")!;
|
||||||
const [subtitle, setSubtitle] = useSetting("subtitleLanguage")!;
|
const [subtitle, setSubtitle] = useSetting("subtitleLanguage")!;
|
||||||
const languages = new Intl.DisplayNames([i18n.language ?? "en"], {
|
const getLanguageName = useLanguageName();
|
||||||
type: "language",
|
|
||||||
languageDisplay: "standard",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer title={t("settings.playback.label")}>
|
<SettingsContainer title={t("settings.playback.label")}>
|
||||||
@ -57,7 +50,7 @@ export const PlaybackSettings = () => {
|
|||||||
value={playMode}
|
value={playMode}
|
||||||
onValueChange={(value) => setDefaultPlayMode(value)}
|
onValueChange={(value) => setDefaultPlayMode(value)}
|
||||||
values={["direct", "auto"]}
|
values={["direct", "auto"]}
|
||||||
getLabel={(key) => t(`player.${key}`)}
|
getLabel={(key) => t(`player.${key}` as any)}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
<Preference
|
<Preference
|
||||||
@ -71,7 +64,7 @@ export const PlaybackSettings = () => {
|
|||||||
onValueChange={(value) => setAudio(value)}
|
onValueChange={(value) => setAudio(value)}
|
||||||
values={["default", ...allLanguages]}
|
values={["default", ...allLanguages]}
|
||||||
getLabel={(key) =>
|
getLabel={(key) =>
|
||||||
key === "default" ? t("mediainfo.default") : languages.of(key) ?? key
|
key === "default" ? t("mediainfo.default") : getLanguageName(key) ?? key
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</Preference>
|
||||||
@ -90,7 +83,7 @@ export const PlaybackSettings = () => {
|
|||||||
? t("settings.playback.subtitleLanguage.none")
|
? t("settings.playback.subtitleLanguage.none")
|
||||||
: key === "default"
|
: key === "default"
|
||||||
? t("mediainfo.default")
|
? t("mediainfo.default")
|
||||||
: languages.of(key) ?? key
|
: getLanguageName(key) ?? key
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Preference>
|
</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": {
|
"home": {
|
||||||
"recommended": "Polecane",
|
"recommended": "Polecane",
|
||||||
"news": "",
|
|
||||||
"watchlist": "Kontynuuj oglądanie",
|
"watchlist": "Kontynuuj oglądanie",
|
||||||
"info": "Zobacz więcej",
|
"info": "Zobacz więcej",
|
||||||
"none": "Brak odcinków",
|
"none": "Brak odcinków",
|
||||||
"watchlistLogin": "Żeby śledzić co było oglądane albo co planujesz oglądać musisz się zalogować.",
|
"watchlistLogin": "Żeby śledzić co było oglądane albo co planujesz oglądać musisz się zalogować.",
|
||||||
"refreshMetadata": "Odśwież metadane",
|
"refreshMetadata": "Odśwież metadane"
|
||||||
"episodeMore": {
|
|
||||||
"goToShow": "",
|
|
||||||
"download": "",
|
|
||||||
"mediainfo": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"play": "Odtwórz",
|
"play": "Odtwórz",
|
||||||
"trailer": "Odtwórz zwiastun",
|
"trailer": "Odtwórz zwiastun",
|
||||||
"studio": "",
|
"episode-none": "Brak odcinków w tym sezonie"
|
||||||
"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": ""
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"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": {
|
"about": {
|
||||||
"label": "",
|
|
||||||
"android-app": {
|
|
||||||
"label": "",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"git": {
|
"git": {
|
||||||
"label": "",
|
|
||||||
"description": "Otwórz repozytorium github gdzie możesz przejrzeć kod kyoo"
|
"description": "Otwórz repozytorium github gdzie możesz przejrzeć kod kyoo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"back": "",
|
"fullscreen": "Pełny ekran"
|
||||||
"previous": "",
|
|
||||||
"next": "",
|
|
||||||
"play": "",
|
|
||||||
"pause": "",
|
|
||||||
"mute": "",
|
|
||||||
"volume": "",
|
|
||||||
"quality": "",
|
|
||||||
"audios": "",
|
|
||||||
"subtitles": "",
|
|
||||||
"subtitle-none": "",
|
|
||||||
"fullscreen": "Pełny ekran",
|
|
||||||
"direct": "",
|
|
||||||
"transmux": "",
|
|
||||||
"auto": "",
|
|
||||||
"notInPristine": "",
|
|
||||||
"unsupportedError": ""
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"empty": ""
|
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "",
|
|
||||||
"register": "Zarejestruj",
|
"register": "Zarejestruj",
|
||||||
"guest": "Kontynuuj jako gość",
|
"guest": "Kontynuuj jako gość",
|
||||||
"guest-forbidden": "Ta instancja kyoo nie zezwala na korzystanie jako gość",
|
"guest-forbidden": "Ta instancja kyoo nie zezwala na korzystanie jako gość",
|
||||||
@ -186,59 +34,11 @@
|
|||||||
"username": "Nazwa użytkownika",
|
"username": "Nazwa użytkownika",
|
||||||
"password": "Hasło",
|
"password": "Hasło",
|
||||||
"confirm": "Powtórz hasło",
|
"confirm": "Powtórz hasło",
|
||||||
"or-register": "",
|
"delete": "Usuń konto"
|
||||||
"or-login": "",
|
|
||||||
"password-no-match": "",
|
|
||||||
"delete": "Usuń konto",
|
|
||||||
"delete-confirmation": ""
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"empty": "",
|
|
||||||
"error": "",
|
|
||||||
"delete": "",
|
|
||||||
"deleteMessage": "",
|
|
||||||
"pause": "",
|
|
||||||
"resume": "",
|
|
||||||
"retry": ""
|
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"connection": "",
|
|
||||||
"connection-tips": "",
|
|
||||||
"unknown": "Nieznany błąd",
|
"unknown": "Nieznany błąd",
|
||||||
"try-again": "Spróbuj ponownie",
|
"try-again": "Spróbuj ponownie",
|
||||||
"re-login": "Zaloguj się 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": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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