Manually fix translations and update node packages (#502)

This commit is contained in:
Zoe Roux 2024-05-20 00:36:03 +02:00 committed by GitHub
commit c8cc2fc057
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 2197 additions and 1274 deletions

View File

@ -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 }) => {

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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;
};

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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"] });
},

View File

@ -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 {

View File

@ -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({

View File

@ -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"
}
}

View File

@ -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(() => {

View File

@ -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";

View File

@ -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";

View File

@ -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>
);
};

View File

@ -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": {

View File

@ -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";

View File

@ -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}
/>

View File

@ -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 (

View File

@ -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";

View File

@ -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,
});

View File

@ -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 };
}
}

View File

@ -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 };
}
}

View File

@ -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();

View File

@ -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,

View File

@ -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)}

View File

@ -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}

View File

@ -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}

View File

@ -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)}
/>

View File

@ -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))}
/>

View File

@ -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>

View File

@ -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>

View 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})`;
};
};

View 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 },
};

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff