mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Update code to fix biome errors
This commit is contained in:
parent
871aaa032d
commit
bd71be580d
@ -42,9 +42,6 @@ const config = {
|
|||||||
icon: "./assets/icon.png",
|
icon: "./assets/icon.png",
|
||||||
userInterfaceStyle: "automatic",
|
userInterfaceStyle: "automatic",
|
||||||
splash,
|
splash,
|
||||||
updates: {
|
|
||||||
fallbackToCacheTimeout: 0,
|
|
||||||
},
|
|
||||||
assetBundlePatterns: ["**/*"],
|
assetBundlePatterns: ["**/*"],
|
||||||
ios: {
|
ios: {
|
||||||
supportsTablet: true,
|
supportsTablet: true,
|
||||||
@ -59,6 +56,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
|
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||||
|
fallbackToCacheTimeout: 0,
|
||||||
},
|
},
|
||||||
runtimeVersion: {
|
runtimeVersion: {
|
||||||
policy: "sdkVersion",
|
policy: "sdkVersion",
|
||||||
|
@ -30,7 +30,7 @@ export default function PublicLayout() {
|
|||||||
const { error } = useContext(ConnectionErrorContext);
|
const { error } = useContext(ConnectionErrorContext);
|
||||||
const oldAccount = useRef<Account | null>(account);
|
const oldAccount = useRef<Account | null>(account);
|
||||||
|
|
||||||
if (account && !error && account != oldAccount.current) return <Redirect href="/" />;
|
if (account && !error && account !== oldAccount.current) return <Redirect href="/" />;
|
||||||
oldAccount.current = account;
|
oldAccount.current = account;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { getDefaultConfig } = require("expo/metro-config");
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
const path = require("path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const projectRoot = __dirname;
|
const projectRoot = __dirname;
|
||||||
const defaultConfig = getDefaultConfig(projectRoot);
|
const defaultConfig = getDefaultConfig(projectRoot);
|
||||||
|
@ -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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("node:path");
|
||||||
const CopyPlugin = require("copy-webpack-plugin");
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
const DefinePlugin = require("webpack").DefinePlugin;
|
const DefinePlugin = require("webpack").DefinePlugin;
|
||||||
|
|
||||||
|
@ -56,8 +56,6 @@
|
|||||||
"@types/react": "18.2.48",
|
"@types/react": "18.2.48",
|
||||||
"@types/react-dom": "18.2.18",
|
"@types/react-dom": "18.2.18",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"eslint": "^8.56.0",
|
|
||||||
"eslint-config-next": "14.1.0",
|
|
||||||
"react-native": "0.73.2",
|
"react-native": "0.73.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"webpack": "^5.90.0"
|
"webpack": "^5.90.0"
|
||||||
|
@ -40,18 +40,15 @@ export const withTranslations = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppWithTranslations = (props: AppProps) => {
|
const AppWithTranslations = (props: AppProps) => {
|
||||||
const li18n = useMemo(
|
const li18n = useMemo(() => {
|
||||||
() =>
|
if (typeof window === "undefined") return i18n;
|
||||||
typeof window === "undefined"
|
i18next.init({
|
||||||
? i18n
|
...commonOptions,
|
||||||
: (i18next.init({
|
lng: props.pageProps.__lang,
|
||||||
...commonOptions,
|
resources: props.pageProps.__resources,
|
||||||
lng: props.pageProps.__lang,
|
});
|
||||||
resources: props.pageProps.__resources,
|
return i18next;
|
||||||
}),
|
}, [props.pageProps.__lang, props.pageProps.__resources]);
|
||||||
i18next),
|
|
||||||
[props.pageProps.__lang, props.pageProps.__resources],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={li18n}>
|
<I18nextProvider i18n={li18n}>
|
||||||
|
@ -114,7 +114,6 @@ const GlobalCssTheme = () => {
|
|||||||
|
|
||||||
const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
|
const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
|
||||||
if (typeof window === "undefined") return children;
|
if (typeof window === "undefined") return children;
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const registry = useStyleRegistry();
|
const registry = useStyleRegistry();
|
||||||
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
|
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
|
||||||
};
|
};
|
||||||
|
@ -24,10 +24,18 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"noNonNullAssertion": "off",
|
"noNonNullAssertion": "off",
|
||||||
"useImportType": "off",
|
"useImportType": "off",
|
||||||
"noParameterAssign": "off"
|
"noParameterAssign": "off",
|
||||||
|
"useEnumInitializers": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noExplicitAny": "off"
|
"noExplicitAny": "off",
|
||||||
|
"noArrayIndexKey": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"noBannedTypes": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
@ -10,13 +10,10 @@
|
|||||||
"build:mobile:apk": "yarn workspace mobile build:apk",
|
"build:mobile:apk": "yarn workspace mobile build:apk",
|
||||||
"build:mobile:dev": "yarn workspace mobile build:dev",
|
"build:mobile:dev": "yarn workspace mobile build:dev",
|
||||||
"update": "yarn workspace mobile update",
|
"update": "yarn workspace mobile update",
|
||||||
"lint": "eslint .",
|
"lint": "biome lint .",
|
||||||
"format": "prettier -c .",
|
"format": "biome format .",
|
||||||
"format:fix": "prettier -w ."
|
"format:fix": "biome format . --write"
|
||||||
},
|
},
|
||||||
"eslintIgnore": [
|
|
||||||
"next-env.d.ts"
|
|
||||||
],
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
@ -49,8 +49,8 @@ export const setCookie = (key: string, val?: unknown) => {
|
|||||||
const d = new Date();
|
const d = new Date();
|
||||||
// A year
|
// A year
|
||||||
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||||
const expires = value ? "expires=" + d.toUTCString() : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
const expires = value ? `expires=${d.toUTCString()}` : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
|
||||||
document.cookie = key + "=" + value + ";" + expires + ";path=/;samesite=strict";
|
document.cookie = `${key}=${value};${expires};path=/;samesite=strict`;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,10 +65,10 @@ export const readCookie = <T extends ZodTypeAny>(
|
|||||||
const ca = decodedCookie.split(";");
|
const ca = decodedCookie.split(";");
|
||||||
for (let i = 0; i < ca.length; i++) {
|
for (let i = 0; i < ca.length; i++) {
|
||||||
let c = ca[i];
|
let c = ca[i];
|
||||||
while (c.charAt(0) == " ") {
|
while (c.charAt(0) === " ") {
|
||||||
c = c.substring(1);
|
c = c.substring(1);
|
||||||
}
|
}
|
||||||
if (c.indexOf(name) == 0) {
|
if (c.indexOf(name) === 0) {
|
||||||
const str = c.substring(name.length, c.length);
|
const str = c.substring(name.length, c.length);
|
||||||
return parser ? parser.parse(JSON.parse(str)) : str;
|
return parser ? parser.parse(JSON.parse(str)) : str;
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ export const addAccount = (account: Account) => {
|
|||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
|
|
||||||
// Prevent the user from adding the same account twice.
|
// Prevent the user from adding the same account twice.
|
||||||
if (accounts.find((x) => x.id == account.id)) {
|
if (accounts.find((x) => x.id === account.id)) {
|
||||||
updateAccount(account.id, account);
|
updateAccount(account.id, account);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
|||||||
|
|
||||||
export const updateAccount = (id: string, account: Account) => {
|
export const updateAccount = (id: string, account: Account) => {
|
||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
const idx = accounts.findIndex((x) => x.id == id);
|
const idx = accounts.findIndex((x) => x.id === id);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
const selected = account.selected;
|
const selected = account.selected;
|
||||||
|
@ -72,7 +72,6 @@ export const ConnectionErrorContext = createContext<{
|
|||||||
setError: (error: KyooErrors) => void;
|
setError: (error: KyooErrors) => void;
|
||||||
}>({ error: null, loading: true, setError: () => {} });
|
}>({ error: null, loading: true, setError: () => {} });
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
export const AccountProvider = ({
|
export const AccountProvider = ({
|
||||||
children,
|
children,
|
||||||
ssrAccount,
|
ssrAccount,
|
||||||
@ -115,7 +114,7 @@ export const AccountProvider = ({
|
|||||||
acc?.map((account) => ({
|
acc?.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||||
remove: () => removeAccounts((x) => x.id == x.id),
|
remove: () => removeAccounts((x) => x.id === account.id),
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
[acc],
|
[acc],
|
||||||
);
|
);
|
||||||
@ -151,6 +150,7 @@ export const AccountProvider = ({
|
|||||||
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 (
|
||||||
|
// biome-ignore lint/suspicious/noDoubleEquals: id can be an id, null or undefined
|
||||||
selected?.id != oldSelected.current?.id ||
|
selected?.id != oldSelected.current?.id ||
|
||||||
(userIsError && selected?.token.access_token !== oldSelected.current?.token)
|
(userIsError && selected?.token.access_token !== oldSelected.current?.token)
|
||||||
) {
|
) {
|
||||||
|
@ -94,7 +94,7 @@ let running: ReturnType<typeof getTokenWJ> | null = null;
|
|||||||
|
|
||||||
export const getTokenWJ = async (
|
export const getTokenWJ = async (
|
||||||
acc?: Account | null,
|
acc?: Account | null,
|
||||||
forceRefresh: boolean = false,
|
forceRefresh = false,
|
||||||
): Promise<readonly [string, Token, null] | readonly [null, null, KyooErrors | null]> => {
|
): Promise<readonly [string, Token, null] | readonly [null, null, KyooErrors | null]> => {
|
||||||
if (acc === undefined) acc = getCurrentAccount();
|
if (acc === undefined) acc = getCurrentAccount();
|
||||||
if (!acc) return [null, null, null] as const;
|
if (!acc) return [null, null, null] as const;
|
||||||
@ -150,6 +150,7 @@ export const useToken = () => {
|
|||||||
account ? `${account.token.token_type} ${account.token.access_token}` : null,
|
account ? `${account.token.token_type} ${account.token.access_token}` : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Refresh token when account change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function run() {
|
async function run() {
|
||||||
const nToken = await getTokenWJ();
|
const nToken = await getTokenWJ();
|
||||||
|
@ -66,7 +66,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
.join("/")
|
.join("/")
|
||||||
.replace("//", "/")
|
.replace("//", "/")
|
||||||
.replace("/?", "?");
|
.replace("/?", "?");
|
||||||
let resp;
|
let resp: Response;
|
||||||
try {
|
try {
|
||||||
resp = await fetch(path, {
|
resp = await fetch(path, {
|
||||||
method: context.method,
|
method: context.method,
|
||||||
@ -97,11 +97,11 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
if (resp.status === 403 && iToken === undefined && token) {
|
if (resp.status === 403 && iToken === undefined && token) {
|
||||||
const [newToken, _, error] = await getTokenWJ(undefined, true);
|
const [newToken, _, error] = await getTokenWJ(undefined, true);
|
||||||
if (newToken) return await queryFn(context, type, newToken);
|
if (newToken) return await queryFn(context, type, newToken);
|
||||||
else console.error("refresh error while retrying a forbidden", error);
|
console.error("refresh error while retrying a forbidden", error);
|
||||||
}
|
}
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const error = await resp.text();
|
const error = await resp.text();
|
||||||
let data;
|
let data: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(error);
|
data = JSON.parse(error);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -122,7 +122,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
|
|
||||||
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown;
|
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown;
|
||||||
|
|
||||||
let data;
|
let data: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
data = await resp.json();
|
data = await resp.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -204,11 +204,10 @@ export const toQueryKey = (query: {
|
|||||||
query.options?.apiUrl,
|
query.options?.apiUrl,
|
||||||
...query.path,
|
...query.path,
|
||||||
query.params
|
query.params
|
||||||
? "?" +
|
? `?${Object.entries(query.params)
|
||||||
Object.entries(query.params)
|
|
||||||
.filter(([_, v]) => v !== undefined)
|
.filter(([_, v]) => v !== undefined)
|
||||||
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
||||||
.join("&")
|
.join("&")}`
|
||||||
: null,
|
: null,
|
||||||
].filter((x) => x);
|
].filter((x) => x);
|
||||||
};
|
};
|
||||||
@ -267,12 +266,11 @@ export const fetchQuery = async (queries: QueryIdentifier[], authToken?: string
|
|||||||
queryFn: (ctx) => queryFn(ctx, Paged(query.parser), authToken),
|
queryFn: (ctx) => queryFn(ctx, Paged(query.parser), authToken),
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return client.prefetchQuery({
|
|
||||||
queryKey: toQueryKey(query),
|
|
||||||
queryFn: (ctx) => queryFn(ctx, query.parser, authToken),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return client.prefetchQuery({
|
||||||
|
queryKey: toQueryKey(query),
|
||||||
|
queryFn: (ctx) => queryFn(ctx, query.parser, authToken),
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return client;
|
return client;
|
||||||
|
@ -196,8 +196,9 @@ export const WatchInfoP = z
|
|||||||
|
|
||||||
// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
||||||
const humanFileSize = (size: number): string => {
|
const humanFileSize = (size: number): string => {
|
||||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||||
// @ts-ignore I'm not gonna fix stackoverflow's working code.
|
// @ts-ignore I'm not gonna fix stackoverflow's working code.
|
||||||
|
// biome-ignore lint: same as above
|
||||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
|
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ import { Platform } from "react-native";
|
|||||||
|
|
||||||
export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
|
export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
|
||||||
if (Platform.OS === "web" && typeof window === "undefined" && ssrTheme) return ssrTheme;
|
if (Platform.OS === "web" && typeof window === "undefined" && ssrTheme) return ssrTheme;
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const [value] = useMMKVString("theme", storage);
|
const [value] = useMMKVString("theme", storage);
|
||||||
if (!value) return "auto";
|
if (!value) return "auto";
|
||||||
return value as "light" | "dark" | "auto";
|
return value as "light" | "dark" | "auto";
|
||||||
|
@ -38,7 +38,8 @@ export const getDisplayDate = (data: Show | Movie) => {
|
|||||||
return startAir.getFullYear().toString();
|
return startAir.getFullYear().toString();
|
||||||
}
|
}
|
||||||
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
||||||
} else if (airDate) {
|
}
|
||||||
|
if (airDate) {
|
||||||
return airDate.getFullYear().toString();
|
return airDate.getFullYear().toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
import { type AlertButton, type AlertOptions } from "react-native";
|
import { type AlertButton, type AlertOptions } from "react-native";
|
||||||
import Swal, { type SweetAlertIcon } from "sweetalert2";
|
import Swal, { type SweetAlertIcon } from "sweetalert2";
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn
|
||||||
export class Alert {
|
export class Alert {
|
||||||
static alert(
|
static alert(
|
||||||
title: string,
|
title: string,
|
||||||
|
@ -27,14 +27,13 @@ import { ComponentType, forwardRef, RefAttributes } from "react";
|
|||||||
|
|
||||||
const stringToColor = (string: string) => {
|
const stringToColor = (string: string) => {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
let i;
|
|
||||||
|
|
||||||
for (i = 0; i < string.length; i += 1) {
|
for (let i = 0; i < string.length; i += 1) {
|
||||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = "#";
|
let color = "#";
|
||||||
for (i = 0; i < 3; i += 1) {
|
for (let i = 0; i < 3; i += 1) {
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
color += `00${value.toString(16)}`.slice(-2);
|
color += `00${value.toString(16)}`.slice(-2);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export const Chip = ({
|
|||||||
|
|
||||||
textProps ??= {};
|
textProps ??= {};
|
||||||
|
|
||||||
const sizeMult = size == "medium" ? 1 : size == "small" ? 0.5 : 1.5;
|
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
@ -33,7 +33,7 @@ export type Props = {
|
|||||||
src?: KyooImage | null;
|
src?: KyooImage | null;
|
||||||
quality: "low" | "medium" | "high";
|
quality: "low" | "medium" | "high";
|
||||||
alt?: string;
|
alt?: string;
|
||||||
Error?: ReactElement | null;
|
Err?: ReactElement | null;
|
||||||
forcedLoading?: boolean;
|
forcedLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,17 +22,14 @@ import { decode } from "blurhash";
|
|||||||
import {
|
import {
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
createElement,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useYoshiki } from "yoshiki";
|
import { useYoshiki } from "yoshiki";
|
||||||
import { Stylable, nativeStyleToCss } from "yoshiki/native";
|
import { nativeStyleToCss } from "yoshiki/native";
|
||||||
import { StyleList, processStyleList } from "yoshiki/src/type";
|
|
||||||
|
|
||||||
// The blurhashToUrl has been stolen from https://gist.github.com/mattiaz9/53cb67040fa135cb395b1d015a200aff
|
// The blurhashToUrl has been stolen from https://gist.github.com/mattiaz9/53cb67040fa135cb395b1d015a200aff
|
||||||
export function blurHashToDataURL(hash: string | undefined): string | undefined {
|
export function blurHashToDataURL(hash: string | undefined): string | undefined {
|
||||||
@ -53,7 +50,7 @@ function parsePixels(pixels: Uint8ClampedArray, width: number, height: number) {
|
|||||||
typeof Buffer !== "undefined"
|
typeof Buffer !== "undefined"
|
||||||
? Buffer.from(getPngArray(pngString)).toString("base64")
|
? Buffer.from(getPngArray(pngString)).toString("base64")
|
||||||
: btoa(pngString);
|
: btoa(pngString);
|
||||||
return "data:image/png;base64," + dataURL;
|
return `data:image/png;base64,${dataURL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPngArray(pngString: string) {
|
function getPngArray(pngString: string) {
|
||||||
@ -70,6 +67,7 @@ function generatePng(width: number, height: number, rgbaString: string) {
|
|||||||
const SIGNATURE = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);
|
const SIGNATURE = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);
|
||||||
const NO_FILTER = String.fromCharCode(0);
|
const NO_FILTER = String.fromCharCode(0);
|
||||||
|
|
||||||
|
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||||
let n, c, k;
|
let n, c, k;
|
||||||
|
|
||||||
// make crc table
|
// make crc table
|
||||||
@ -89,7 +87,9 @@ function generatePng(width: number, height: number, rgbaString: string) {
|
|||||||
function inflateStore(data: string) {
|
function inflateStore(data: string) {
|
||||||
const MAX_STORE_LENGTH = 65535;
|
const MAX_STORE_LENGTH = 65535;
|
||||||
let storeBuffer = "";
|
let storeBuffer = "";
|
||||||
|
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||||
let remaining;
|
let remaining;
|
||||||
|
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||||
let blockType;
|
let blockType;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += MAX_STORE_LENGTH) {
|
for (let i = 0; i < data.length; i += MAX_STORE_LENGTH) {
|
||||||
@ -113,7 +113,7 @@ function generatePng(width: number, height: number, rgbaString: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adler32(data: string) {
|
function adler32(data: string) {
|
||||||
let MOD_ADLER = 65521;
|
const MOD_ADLER = 65521;
|
||||||
let a = 1;
|
let a = 1;
|
||||||
let b = 0;
|
let b = 0;
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ function generatePng(width: number, height: number, rgbaString: string) {
|
|||||||
const IHDR = createIHDR(width, height);
|
const IHDR = createIHDR(width, height);
|
||||||
|
|
||||||
let scanlines = "";
|
let scanlines = "";
|
||||||
let scanline;
|
let scanline: string;
|
||||||
|
|
||||||
for (let y = 0; y < rgbaString.length; y += width * 4) {
|
for (let y = 0; y < rgbaString.length; y += width * 4) {
|
||||||
scanline = NO_FILTER;
|
scanline = NO_FILTER;
|
||||||
|
@ -33,7 +33,7 @@ export const Image = ({
|
|||||||
alt,
|
alt,
|
||||||
forcedLoading = false,
|
forcedLoading = false,
|
||||||
layout,
|
layout,
|
||||||
Error,
|
Err,
|
||||||
...props
|
...props
|
||||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
@ -53,8 +53,8 @@ export const Image = ({
|
|||||||
|
|
||||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||||
if (!src || state === "errored") {
|
if (!src || state === "errored") {
|
||||||
return Error !== undefined ? (
|
return Err !== undefined ? (
|
||||||
Error
|
Err
|
||||||
) : (
|
) : (
|
||||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||||
);
|
);
|
||||||
|
@ -33,7 +33,7 @@ export const Image = ({
|
|||||||
alt,
|
alt,
|
||||||
forcedLoading = false,
|
forcedLoading = false,
|
||||||
layout,
|
layout,
|
||||||
Error,
|
Err,
|
||||||
...props
|
...props
|
||||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
@ -45,8 +45,8 @@ export const Image = ({
|
|||||||
|
|
||||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||||
if (!src || state === "errored") {
|
if (!src || state === "errored") {
|
||||||
return Error !== undefined ? (
|
return Err !== undefined ? (
|
||||||
Error
|
Err
|
||||||
) : (
|
) : (
|
||||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||||
);
|
);
|
||||||
|
@ -111,7 +111,7 @@ export const ImageBackground = <AsProps = ViewProps,>({
|
|||||||
forcedLoading={forcedLoading}
|
forcedLoading={forcedLoading}
|
||||||
alt={alt!}
|
alt={alt!}
|
||||||
layout={{ width: percent(100), height: percent(100) }}
|
layout={{ width: percent(100), height: percent(100) }}
|
||||||
Error={hideLoad ? null : undefined}
|
Err={hideLoad ? null : undefined}
|
||||||
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
|
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
|
||||||
style: ImageStyle;
|
style: ImageStyle;
|
||||||
})}
|
})}
|
||||||
|
@ -22,7 +22,7 @@ import { forwardRef, ReactNode } from "react";
|
|||||||
import { Platform, Pressable, TextProps, View, PressableProps, Linking } from "react-native";
|
import { Platform, Pressable, TextProps, View, PressableProps, Linking } from "react-native";
|
||||||
import { TextLink, useLink } from "solito/link";
|
import { TextLink, useLink } from "solito/link";
|
||||||
import { useTheme, useYoshiki } from "yoshiki/native";
|
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||||
import type { UrlObject } from "url";
|
import type { UrlObject } from "node:url";
|
||||||
import { alpha } from "./themes";
|
import { alpha } from "./themes";
|
||||||
import { parseNextPath } from "solito/router";
|
import { parseNextPath } from "solito/router";
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ const Menu = <AsProps,>({
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const alreadyRendered = useRef(false);
|
const alreadyRendered = useRef(false);
|
||||||
const [isOpen, setOpen] =
|
const [isOpen, setOpen] =
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
||||||
|
|
||||||
// deos the same as a useMemo but for props.
|
// deos the same as a useMemo but for props.
|
||||||
|
@ -111,9 +111,7 @@ const selectMode = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const auto = useAutomaticTheme("theme", { light, dark });
|
const auto = useAutomaticTheme("theme", { light, dark });
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const alternate = useAutomaticTheme("alternate", { dark: light, light: dark });
|
const alternate = useAutomaticTheme("alternate", { dark: light, light: dark });
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
|
@ -31,7 +31,7 @@ export const Head = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<NextHead>
|
<NextHead>
|
||||||
{title && <title>{title + " - Kyoo"}</title>}
|
{title && <title>{`${title} - Kyoo`}</title>}
|
||||||
{description && <meta name="description" content={description} />}
|
{description && <meta name="description" content={description} />}
|
||||||
{image && <meta property="og:image" content={image} />}
|
{image && <meta property="og:image" content={image} />}
|
||||||
</NextHead>
|
</NextHead>
|
||||||
|
@ -82,7 +82,7 @@ export const Scanner = () => {
|
|||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
{data != null && data.length == 0 && <P>{t("admin.scanner.empty")}</P>}
|
{data != null && data.length === 0 && <P>{t("admin.scanner.empty")}</P>}
|
||||||
</>
|
</>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
|
@ -121,14 +121,14 @@ export const BrowseSettings = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={GridView}
|
icon={GridView}
|
||||||
onPress={() => setLayout(Layout.Grid)}
|
onPress={() => setLayout(Layout.Grid)}
|
||||||
color={layout == Layout.Grid ? theme.accent : undefined}
|
color={layout === Layout.Grid ? theme.accent : undefined}
|
||||||
{...tooltip(t("browse.switchToGrid"))}
|
{...tooltip(t("browse.switchToGrid"))}
|
||||||
{...css({ padding: ts(0.5), marginY: "auto" })}
|
{...css({ padding: ts(0.5), marginY: "auto" })}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={ViewList}
|
icon={ViewList}
|
||||||
onPress={() => setLayout(Layout.List)}
|
onPress={() => setLayout(Layout.List)}
|
||||||
color={layout == Layout.List ? theme.accent : undefined}
|
color={layout === Layout.List ? theme.accent : undefined}
|
||||||
{...tooltip(t("browse.switchToList"))}
|
{...tooltip(t("browse.switchToList"))}
|
||||||
{...css({ padding: ts(0.5), marginY: "auto" })}
|
{...css({ padding: ts(0.5), marginY: "auto" })}
|
||||||
/>
|
/>
|
||||||
|
@ -35,15 +35,14 @@ const MediaInfoTable = ({
|
|||||||
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)[], s: string) => {
|
||||||
if (trackTable.length == 0) {
|
if (trackTable.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const singleTrack = trackTable.length == 1;
|
const singleTrack = trackTable.length === 1;
|
||||||
return trackTable.reduce(
|
return trackTable.reduce(
|
||||||
(collected, audioTrack, index) => ({
|
(collected, audioTrack, index) => {
|
||||||
...collected,
|
|
||||||
// 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
|
||||||
[singleTrack ? t(s) : `${t(s)} ${index + 1}`]: [
|
collected[singleTrack ? t(s) : `${t(s)} ${index + 1}`] = [
|
||||||
audioTrack.displayName,
|
audioTrack.displayName,
|
||||||
// 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,
|
||||||
@ -51,8 +50,9 @@ const MediaInfoTable = ({
|
|||||||
audioTrack.codec,
|
audioTrack.codec,
|
||||||
]
|
]
|
||||||
.filter((x) => x !== undefined)
|
.filter((x) => x !== undefined)
|
||||||
.join(" - "),
|
.join(" - ");
|
||||||
}),
|
return collected;
|
||||||
|
},
|
||||||
{} as Record<string, string | undefined>,
|
{} as Record<string, string | undefined>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -94,7 +94,7 @@ const MediaInfoTable = ({
|
|||||||
<Skeleton>{value ? <P>{value}</P> : undefined}</Skeleton>
|
<Skeleton>{value ? <P>{value}</P> : undefined}</Skeleton>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{index == l.length - 1 && <HR />}
|
{index === l.length - 1 && <HR />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
@ -295,6 +295,7 @@ export const EpisodeLine = ({
|
|||||||
>
|
>
|
||||||
<Skeleton>
|
<Skeleton>
|
||||||
{isLoading || (
|
{isLoading || (
|
||||||
|
// biome-ignore lint/a11y/useValidAriaValues: simply use H6 for the style but keep a P
|
||||||
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
|
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
|
||||||
{[displayNumber, name ?? t("show.episodeNoMetadata")].join(" · ")}
|
{[displayNumber, name ?? t("show.episodeNoMetadata")].join(" · ")}
|
||||||
</H6>
|
</H6>
|
||||||
|
@ -42,7 +42,7 @@ import { KyooImage } from "@kyoo/models";
|
|||||||
import { Atom, useAtomValue } from "jotai";
|
import { Atom, useAtomValue } from "jotai";
|
||||||
import DownloadForOffline from "@material-symbols/svg-400/rounded/download_for_offline.svg";
|
import DownloadForOffline from "@material-symbols/svg-400/rounded/download_for_offline.svg";
|
||||||
import Downloading from "@material-symbols/svg-400/rounded/downloading.svg";
|
import Downloading from "@material-symbols/svg-400/rounded/downloading.svg";
|
||||||
import Error from "@material-symbols/svg-400/rounded/error.svg";
|
import ErrorIcon from "@material-symbols/svg-400/rounded/error.svg";
|
||||||
import NotStarted from "@material-symbols/svg-400/rounded/not_started.svg";
|
import NotStarted from "@material-symbols/svg-400/rounded/not_started.svg";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
@ -126,6 +126,7 @@ const DownloadedItem = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
|
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
|
||||||
|
{/* biome-ignore lint/a11y/useValidAriaValues: use h6 for style only */}
|
||||||
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
|
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
|
||||||
{name ?? t("show.episodeNoMetadata")}
|
{name ?? t("show.episodeNoMetadata")}
|
||||||
</H6>
|
</H6>
|
||||||
@ -193,10 +194,9 @@ const downloadIcon = (status: State["status"]) => {
|
|||||||
case "DOWNLOADING":
|
case "DOWNLOADING":
|
||||||
return Downloading;
|
return Downloading;
|
||||||
case "FAILED":
|
case "FAILED":
|
||||||
return Error;
|
return ErrorIcon;
|
||||||
case "PAUSED":
|
case "PAUSED":
|
||||||
case "STOPPED":
|
case "STOPPED":
|
||||||
default:
|
|
||||||
return NotStarted;
|
return NotStarted;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -38,7 +38,7 @@ import { Player } from "../player";
|
|||||||
import { atom, useSetAtom, PrimitiveAtom, useStore } from "jotai";
|
import { atom, useSetAtom, PrimitiveAtom, useStore } from "jotai";
|
||||||
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
|
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
|
||||||
import { ReactNode, useEffect } from "react";
|
import { ReactNode, useEffect } from "react";
|
||||||
import { Platform, ToastAndroid } from "react-native";
|
import { ToastAndroid } from "react-native";
|
||||||
import { QueryClient, useQueryClient } from "@tanstack/react-query";
|
import { QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Router } from "expo-router/build/types";
|
import { Router } from "expo-router/build/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -257,7 +257,7 @@ export const DownloadProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const dls: { data: Episode | Movie; info: WatchInfo; path: string; state: State }[] =
|
const dls: { data: Episode | Movie; info: WatchInfo; path: string; state: State }[] =
|
||||||
JSON.parse(storage.getString("downloads") ?? "[]");
|
JSON.parse(storage.getString("downloads") ?? "[]");
|
||||||
const downloads = dls.map((dl) => {
|
const downloads = dls.map((dl) => {
|
||||||
const t = tasks.find((x) => x.id == dl.data.id);
|
const t = tasks.find((x) => x.id === dl.data.id);
|
||||||
if (t) return setupDownloadTask(dl, t, store, queryClient);
|
if (t) return setupDownloadTask(dl, t, store, queryClient);
|
||||||
|
|
||||||
const stateAtom = atom({
|
const stateAtom = atom({
|
||||||
|
@ -35,7 +35,7 @@ export const ConnectionError = () => {
|
|||||||
const { error, retry } = useContext(ConnectionErrorContext);
|
const { error, retry } = useContext(ConnectionErrorContext);
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
|
|
||||||
if (error && (error.status === 401 || error.status == 403)) {
|
if (error && (error.status === 401 || error.status === 403)) {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
@ -36,7 +36,7 @@ export const ErrorView = ({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// if this is a permission error, make it go up the tree to have a whole page login screen.
|
// if this is a permission error, make it go up the tree to have a whole page login screen.
|
||||||
if (!noBubble && (error.status === 401 || error.status == 403)) setError(error);
|
if (!noBubble && (error.status === 401 || error.status === 403)) setError(error);
|
||||||
}, [error, noBubble, setError]);
|
}, [error, noBubble, setError]);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return (
|
return (
|
||||||
|
@ -78,6 +78,7 @@ const InfiniteScroll = <Props,>({
|
|||||||
|
|
||||||
// Automatically trigger a scroll check on start and after a fetch end in case the user is already
|
// Automatically trigger a scroll check on start and after a fetch end in case the user is already
|
||||||
// at the bottom of the page or if there is no scroll bar (ultrawide or something like that)
|
// at the bottom of the page or if there is no scroll bar (ultrawide or something like that)
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Check for scroll pause after fetch ends
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onScroll();
|
onScroll();
|
||||||
}, [isFetching, onScroll]);
|
}, [isFetching, onScroll]);
|
||||||
@ -92,13 +93,13 @@ const InfiniteScroll = <Props,>({
|
|||||||
// the as any is due to differencies between css types of native and web (already accounted for in yoshiki)
|
// the as any is due to differencies between css types of native and web (already accounted for in yoshiki)
|
||||||
gridGap: layout.gap as any,
|
gridGap: layout.gap as any,
|
||||||
},
|
},
|
||||||
layout.layout == "vertical" && {
|
layout.layout === "vertical" && {
|
||||||
gridTemplateColumns: "1fr",
|
gridTemplateColumns: "1fr",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
paddingY: layout.gap as any,
|
paddingY: layout.gap as any,
|
||||||
},
|
},
|
||||||
layout.layout == "horizontal" && {
|
layout.layout === "horizontal" && {
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
|
@ -29,9 +29,9 @@ import { DefaultLayout } from "../layout";
|
|||||||
|
|
||||||
export const cleanApiUrl = (apiUrl: string) => {
|
export const cleanApiUrl = (apiUrl: string) => {
|
||||||
if (Platform.OS === "web") return undefined;
|
if (Platform.OS === "web") return undefined;
|
||||||
if (!/https?:\/\//.test(apiUrl)) apiUrl = "http://" + apiUrl;
|
if (!/https?:\/\//.test(apiUrl)) apiUrl = `http://${apiUrl}`;
|
||||||
apiUrl = apiUrl.replace(/\/$/, "");
|
apiUrl = apiUrl.replace(/\/$/, "");
|
||||||
return apiUrl + "/api";
|
return `${apiUrl}/api`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const query: QueryIdentifier<ServerInfo> = {
|
const query: QueryIdentifier<ServerInfo> = {
|
||||||
|
@ -168,7 +168,7 @@ const VolumeSlider = () => {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={
|
icon={
|
||||||
isMuted || volume == 0
|
isMuted || volume === 0
|
||||||
? VolumeOff
|
? VolumeOff
|
||||||
: volume < 25
|
: volume < 25
|
||||||
? VolumeMute
|
? VolumeMute
|
||||||
@ -204,7 +204,12 @@ const ProgressText = (props: Stylable) => {
|
|||||||
|
|
||||||
export const toTimerString = (timer?: number, duration?: number) => {
|
export const toTimerString = (timer?: number, duration?: number) => {
|
||||||
if (!duration) duration = timer;
|
if (!duration) duration = timer;
|
||||||
if (timer === undefined || duration === undefined || isNaN(duration) || isNaN(timer))
|
if (
|
||||||
|
timer === undefined ||
|
||||||
|
duration === undefined ||
|
||||||
|
Number.isNaN(duration) ||
|
||||||
|
Number.isNaN(timer)
|
||||||
|
)
|
||||||
return "??:??";
|
return "??:??";
|
||||||
const h = Math.floor(timer / 3600);
|
const h = Math.floor(timer / 3600);
|
||||||
const min = Math.floor((timer / 60) % 60);
|
const min = Math.floor((timer / 60) % 60);
|
||||||
|
@ -41,7 +41,12 @@ type Thumb = {
|
|||||||
|
|
||||||
const parseTs = (time: string) => {
|
const parseTs = (time: string) => {
|
||||||
const times = time.split(":");
|
const times = time.split(":");
|
||||||
return (parseInt(times[0]) * 3600 + parseInt(times[1]) * 60 + parseFloat(times[2])) * 1000;
|
return (
|
||||||
|
(Number.parseInt(times[0]) * 3600 +
|
||||||
|
Number.parseInt(times[1]) * 60 +
|
||||||
|
Number.parseFloat(times[2])) *
|
||||||
|
1000
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useScrubber = (url: string) => {
|
export const useScrubber = (url: string) => {
|
||||||
@ -64,7 +69,7 @@ export const useScrubber = (url: string) => {
|
|||||||
for (let i = 0; i < ret.length; i++) {
|
for (let i = 0; i < ret.length; i++) {
|
||||||
const times = lines[i * 2].split(" --> ");
|
const times = lines[i * 2].split(" --> ");
|
||||||
const url = lines[i * 2 + 1].split("#xywh=");
|
const url = lines[i * 2 + 1].split("#xywh=");
|
||||||
const xywh = url[1].split(",").map((x) => parseInt(x));
|
const xywh = url[1].split(",").map((x) => Number.parseInt(x));
|
||||||
ret[i] = {
|
ret[i] = {
|
||||||
from: parseTs(times[0]),
|
from: parseTs(times[0]),
|
||||||
to: parseTs(times[1]),
|
to: parseTs(times[1]),
|
||||||
@ -164,7 +169,7 @@ export const BottomScrubber = ({ url, chapters }: { url: string; chapters?: Chap
|
|||||||
<View {...css({ overflow: "hidden" })}>
|
<View {...css({ overflow: "hidden" })}>
|
||||||
<View
|
<View
|
||||||
{...(Platform.OS === "web"
|
{...(Platform.OS === "web"
|
||||||
? css({ transform: `translateX(50%)` })
|
? css({ transform: "translateX(50%)" })
|
||||||
: {
|
: {
|
||||||
// react-native does not support translateX by percentage so we simulate it
|
// react-native does not support translateX by percentage so we simulate it
|
||||||
style: { transform: [{ translateX: scrubberWidth / 2 }] },
|
style: { transform: [{ translateX: scrubberWidth / 2 }] },
|
||||||
|
@ -124,13 +124,11 @@ export const Player = ({
|
|||||||
title={
|
title={
|
||||||
data.type === "movie"
|
data.type === "movie"
|
||||||
? data.name
|
? data.name
|
||||||
: data.show!.name +
|
: `${data.show!.name} ${episodeDisplayNumber({
|
||||||
" " +
|
|
||||||
episodeDisplayNumber({
|
|
||||||
seasonNumber: data.seasonNumber,
|
seasonNumber: data.seasonNumber,
|
||||||
episodeNumber: data.episodeNumber,
|
episodeNumber: data.episodeNumber,
|
||||||
absoluteNumber: data.absoluteNumber,
|
absoluteNumber: data.absoluteNumber,
|
||||||
})
|
})}`
|
||||||
}
|
}
|
||||||
description={data.overview}
|
description={data.overview}
|
||||||
/>
|
/>
|
||||||
|
@ -68,7 +68,7 @@ export const reducerAtom = atom(null, (get, set, action: Action) => {
|
|||||||
case "volume":
|
case "volume":
|
||||||
set(volumeAtom, Math.max(0, Math.min(get(volumeAtom) + action.value, 100)));
|
set(volumeAtom, Math.max(0, Math.min(get(volumeAtom) + action.value, 100)));
|
||||||
break;
|
break;
|
||||||
case "subtitle":
|
case "subtitle": {
|
||||||
const subtitle = get(subtitleAtom);
|
const subtitle = get(subtitleAtom);
|
||||||
const index = subtitle ? action.subtitles.findIndex((x) => x.index === subtitle.index) : -1;
|
const index = subtitle ? action.subtitles.findIndex((x) => x.index === subtitle.index) : -1;
|
||||||
set(
|
set(
|
||||||
@ -76,6 +76,7 @@ export const reducerAtom = atom(null, (get, set, action: Action) => {
|
|||||||
index === -1 ? null : action.subtitles[(index + 1) % action.subtitles.length],
|
index === -1 ? null : action.subtitles[(index + 1) % action.subtitles.length],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -143,22 +143,24 @@ export const Video = memo(function Video({
|
|||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const defaultSubLanguage = account?.settings.subtitleLanguage;
|
const defaultSubLanguage = account?.settings.subtitleLanguage;
|
||||||
const setSubtitle = useSetAtom(subtitleAtom);
|
const setSubtitle = useSetAtom(subtitleAtom);
|
||||||
|
|
||||||
|
// When the video change, try to persist the subtitle language.
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Also include the player ref, it can be initalised after the subtitles.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!subtitles) return;
|
if (!subtitles) return;
|
||||||
setSubtitle((subtitle) => {
|
setSubtitle((subtitle) => {
|
||||||
const subRet = subtitle ? subtitles.find((x) => x.language === subtitle.language) : null;
|
const subRet = subtitle ? subtitles.find((x) => x.language === subtitle.language) : null;
|
||||||
if (subRet) return subRet;
|
if (subRet) return subRet;
|
||||||
if (!defaultSubLanguage) return null;
|
if (!defaultSubLanguage) return null;
|
||||||
if (defaultSubLanguage == "default") return subtitles.find((x) => x.isDefault) ?? null;
|
if (defaultSubLanguage === "default") return subtitles.find((x) => x.isDefault) ?? null;
|
||||||
return subtitles.find((x) => x.language === defaultSubLanguage) ?? null;
|
return subtitles.find((x) => x.language === defaultSubLanguage) ?? null;
|
||||||
});
|
});
|
||||||
// When the video change, try to persist the subtitle language.
|
|
||||||
// Also include the player ref, it can be initalised after the subtitles.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [subtitles, setSubtitle, defaultSubLanguage, ref.current]);
|
}, [subtitles, setSubtitle, defaultSubLanguage, ref.current]);
|
||||||
|
|
||||||
const defaultAudioLanguage = account?.settings.audioLanguage ?? "default";
|
const defaultAudioLanguage = account?.settings.audioLanguage ?? "default";
|
||||||
const setAudio = useSetAtom(audioAtom);
|
const setAudio = useSetAtom(audioAtom);
|
||||||
|
// When the video change, try to persist the subtitle language.
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Also include the player ref, it can be initalised after the subtitles.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!audios) return;
|
if (!audios) return;
|
||||||
setAudio((audio) => {
|
setAudio((audio) => {
|
||||||
@ -172,9 +174,6 @@ export const Video = memo(function Video({
|
|||||||
}
|
}
|
||||||
return audios.find((x) => x.isDefault) ?? audios[0];
|
return audios.find((x) => x.isDefault) ?? audios[0];
|
||||||
});
|
});
|
||||||
// When the video change, try to persist the subtitle language.
|
|
||||||
// Also include the player ref, it can be initalised after the subtitles.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [audios, setAudio, defaultAudioLanguage, ref.current]);
|
}, [audios, setAudio, defaultAudioLanguage, ref.current]);
|
||||||
|
|
||||||
const volume = useAtomValue(volumeAtom);
|
const volume = useAtomValue(volumeAtom);
|
||||||
@ -225,7 +224,7 @@ export const Video = memo(function Video({
|
|||||||
label: t("player.unsupportedError"),
|
label: t("player.unsupportedError"),
|
||||||
duration: 3,
|
duration: 3,
|
||||||
});
|
});
|
||||||
if (mode == PlayMode.Direct) setPlayMode(PlayMode.Hls);
|
if (mode === PlayMode.Direct) setPlayMode(PlayMode.Hls);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -33,9 +33,9 @@ declare module "react-native-video" {
|
|||||||
|
|
||||||
export * from "react-native-video";
|
export * from "react-native-video";
|
||||||
|
|
||||||
import { Audio, Subtitle, getToken, useToken } from "@kyoo/models";
|
import { Audio, Subtitle, useToken } from "@kyoo/models";
|
||||||
import { IconButton, Menu } from "@kyoo/primitives";
|
import { IconButton, Menu } from "@kyoo/primitives";
|
||||||
import { ComponentProps, forwardRef, useEffect, useRef } from "react";
|
import { ComponentProps, forwardRef, useEffect, } from "react";
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import NativeVideo, {
|
import NativeVideo, {
|
||||||
VideoRef,
|
VideoRef,
|
||||||
@ -160,13 +160,13 @@ export const QualitiesMenu = (props: CustomMenu) => {
|
|||||||
<Menu {...props}>
|
<Menu {...props}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={t("player.direct")}
|
label={t("player.direct")}
|
||||||
selected={mode == PlayMode.Direct}
|
selected={mode === PlayMode.Direct}
|
||||||
onSelect={() => setPlayMode(PlayMode.Direct)}
|
onSelect={() => setPlayMode(PlayMode.Direct)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
// TODO: Display the currently selected quality (impossible with rn-video right now)
|
// TODO: Display the currently selected quality (impossible with rn-video right now)
|
||||||
label={t("player.auto")}
|
label={t("player.auto")}
|
||||||
selected={video === -1 && mode == PlayMode.Hls}
|
selected={video === -1 && mode === PlayMode.Hls}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setPlayMode(PlayMode.Hls);
|
setPlayMode(PlayMode.Hls);
|
||||||
setVideo(-1);
|
setVideo(-1);
|
||||||
|
@ -47,13 +47,13 @@ function uuidv4(): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_id = typeof window === "undefined" ? "ssr" : uuidv4();
|
const client_id = typeof window === "undefined" ? "ssr" : uuidv4();
|
||||||
|
|
||||||
const initHls = (): Hls => {
|
const initHls = (): Hls => {
|
||||||
if (hls !== null) return hls;
|
if (hls !== null) return hls;
|
||||||
const loadPolicy: LoadPolicy = {
|
const loadPolicy: LoadPolicy = {
|
||||||
default: {
|
default: {
|
||||||
maxTimeToFirstByteMs: Infinity,
|
maxTimeToFirstByteMs: Number.POSITIVE_INFINITY,
|
||||||
maxLoadTimeMs: 60_000,
|
maxLoadTimeMs: 60_000,
|
||||||
timeoutRetry: {
|
timeoutRetry: {
|
||||||
maxNumRetry: 2,
|
maxNumRetry: 2,
|
||||||
@ -74,14 +74,14 @@ const initHls = (): Hls => {
|
|||||||
xhr.setRequestHeader("X-CLIENT-ID", client_id);
|
xhr.setRequestHeader("X-CLIENT-ID", client_id);
|
||||||
},
|
},
|
||||||
autoStartLoad: false,
|
autoStartLoad: false,
|
||||||
startLevel: Infinity,
|
startLevel: Number.POSITIVE_INFINITY,
|
||||||
abrEwmaDefaultEstimate: 35_000_000,
|
abrEwmaDefaultEstimate: 35_000_000,
|
||||||
abrEwmaDefaultEstimateMax: 50_000_000,
|
abrEwmaDefaultEstimateMax: 50_000_000,
|
||||||
// debug: true,
|
// debug: true,
|
||||||
lowLatencyMode: false,
|
lowLatencyMode: false,
|
||||||
fragLoadPolicy: {
|
fragLoadPolicy: {
|
||||||
default: {
|
default: {
|
||||||
maxTimeToFirstByteMs: Infinity,
|
maxTimeToFirstByteMs: Number.POSITIVE_INFINITY,
|
||||||
maxLoadTimeMs: 60_000,
|
maxLoadTimeMs: 60_000,
|
||||||
timeoutRetry: {
|
timeoutRetry: {
|
||||||
maxNumRetry: 5,
|
maxNumRetry: 5,
|
||||||
@ -148,6 +148,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
const subtitle = useAtomValue(subtitleAtom);
|
const subtitle = useAtomValue(subtitleAtom);
|
||||||
useSubtitle(ref, subtitle, fonts);
|
useSubtitle(ref, subtitle, fonts);
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: onError changes should not restart the playback.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!ref?.current || !source.uri) return;
|
if (!ref?.current || !source.uri) return;
|
||||||
if (!hls || oldHls.current !== source.hls) {
|
if (!hls || oldHls.current !== source.hls) {
|
||||||
@ -172,12 +173,11 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// onError changes should not restart the playback.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [source.uri, source.hls]);
|
}, [source.uri, source.hls]);
|
||||||
|
|
||||||
const mode = useAtomValue(playModeAtom);
|
const mode = useAtomValue(playModeAtom);
|
||||||
const audio = useAtomValue(audioAtom);
|
const audio = useAtomValue(audioAtom);
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: also change when the mode change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hls) return;
|
if (!hls) return;
|
||||||
const update = () => {
|
const update = () => {
|
||||||
@ -389,12 +389,12 @@ export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
|
|||||||
<Menu {...props}>
|
<Menu {...props}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={t("player.direct")}
|
label={t("player.direct")}
|
||||||
selected={hls === null || mode == PlayMode.Direct}
|
selected={hls === null || mode === PlayMode.Direct}
|
||||||
onSelect={() => setPlayMode(PlayMode.Direct)}
|
onSelect={() => setPlayMode(PlayMode.Direct)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={
|
label={
|
||||||
hls != null && hls.autoLevelEnabled && hls.currentLevel >= 0
|
hls?.autoLevelEnabled && hls.currentLevel >= 0
|
||||||
? `${t("player.auto")} (${levelName(hls.levels[hls.currentLevel], true)})`
|
? `${t("player.auto")} (${levelName(hls.levels[hls.currentLevel], true)})`
|
||||||
: t("player.auto")
|
: t("player.auto")
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ export const WatchStatusObserver = ({
|
|||||||
await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }),
|
await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }),
|
||||||
});
|
});
|
||||||
const mutate = useCallback(
|
const mutate = useCallback(
|
||||||
(seconds: number) =>
|
(type: string, slug: string, seconds: number) =>
|
||||||
_mutate({
|
_mutate({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: [type, slug, "watchStatus"],
|
path: [type, slug, "watchStatus"],
|
||||||
@ -52,7 +52,7 @@ export const WatchStatusObserver = ({
|
|||||||
percent: Math.round((seconds / duration) * 100),
|
percent: Math.round((seconds / duration) * 100),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[_mutate, type, slug, duration],
|
[_mutate, duration],
|
||||||
);
|
);
|
||||||
const readProgress = useAtomCallback(
|
const readProgress = useAtomCallback(
|
||||||
useCallback((get) => {
|
useCallback((get) => {
|
||||||
@ -65,19 +65,20 @@ export const WatchStatusObserver = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
mutate(readProgress());
|
mutate(type, slug, readProgress());
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
mutate(readProgress());
|
mutate(type, slug, readProgress());
|
||||||
};
|
};
|
||||||
}, [account, type, slug, readProgress, mutate]);
|
}, [account, type, slug, readProgress, mutate]);
|
||||||
|
|
||||||
// update watch status when play status change (and on mount).
|
// update watch status when play status change (and on mount).
|
||||||
const isPlaying = useAtomValue(playAtom);
|
const isPlaying = useAtomValue(playAtom);
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Include isPlaying
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
mutate(readProgress());
|
mutate(type, slug, readProgress());
|
||||||
}, [account, type, slug, isPlaying, readProgress, mutate]);
|
}, [account, type, slug, isPlaying, readProgress, mutate]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,7 @@ const query = (
|
|||||||
params: {
|
params: {
|
||||||
q: query,
|
q: query,
|
||||||
sortBy:
|
sortBy:
|
||||||
sortKey && sortKey != SearchSort.Relevance ? `${sortKey}:${sortOrd ?? "asc"}` : undefined,
|
sortKey && sortKey !== SearchSort.Relevance ? `${sortKey}:${sortOrd ?? "asc"}` : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ function dataURItoBlob(dataURI: string) {
|
|||||||
const byteString = atob(dataURI.split(",")[1]);
|
const byteString = atob(dataURI.split(",")[1]);
|
||||||
const ab = new ArrayBuffer(byteString.length);
|
const ab = new ArrayBuffer(byteString.length);
|
||||||
const ia = new Uint8Array(ab);
|
const ia = new Uint8Array(ab);
|
||||||
for (var i = 0; i < byteString.length; i++) {
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
ia[i] = byteString.charCodeAt(i);
|
ia[i] = byteString.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return new Blob([ab], { type: "image/jpeg" });
|
return new Blob([ab], { type: "image/jpeg" });
|
||||||
|
@ -31,7 +31,7 @@ import SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-f
|
|||||||
|
|
||||||
// I gave up on finding a way to retrive this using the Intl api (probably does not exist)
|
// 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
|
// Simply copy pasted the list of languages from https://www.localeplanet.com/api/codelist.json
|
||||||
// prettier-ignore
|
// 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 = ["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 = () => {
|
||||||
|
1560
front/yarn.lock
1560
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user