Update code to fix biome errors

This commit is contained in:
Zoe Roux 2024-05-10 14:44:12 +02:00
parent 871aaa032d
commit bd71be580d
No known key found for this signature in database
50 changed files with 184 additions and 1655 deletions

View File

@ -42,9 +42,6 @@ const config = {
icon: "./assets/icon.png",
userInterfaceStyle: "automatic",
splash,
updates: {
fallbackToCacheTimeout: 0,
},
assetBundlePatterns: ["**/*"],
ios: {
supportsTablet: true,
@ -59,6 +56,7 @@ const config = {
},
updates: {
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
fallbackToCacheTimeout: 0,
},
runtimeVersion: {
policy: "sdkVersion",

View File

@ -30,7 +30,7 @@ export default function PublicLayout() {
const { error } = useContext(ConnectionErrorContext);
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;
return (

View File

@ -19,7 +19,7 @@
*/
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
const path = require("node:path");
const projectRoot = __dirname;
const defaultConfig = getDefaultConfig(projectRoot);

View File

@ -18,7 +18,7 @@
* 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 DefinePlugin = require("webpack").DefinePlugin;

View File

@ -56,8 +56,6 @@
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
"copy-webpack-plugin": "^12.0.2",
"eslint": "^8.56.0",
"eslint-config-next": "14.1.0",
"react-native": "0.73.2",
"typescript": "^5.3.3",
"webpack": "^5.90.0"

View File

@ -40,18 +40,15 @@ export const withTranslations = (
};
const AppWithTranslations = (props: AppProps) => {
const li18n = useMemo(
() =>
typeof window === "undefined"
? i18n
: (i18next.init({
...commonOptions,
lng: props.pageProps.__lang,
resources: props.pageProps.__resources,
}),
i18next),
[props.pageProps.__lang, props.pageProps.__resources],
);
const li18n = useMemo(() => {
if (typeof window === "undefined") return i18n;
i18next.init({
...commonOptions,
lng: props.pageProps.__lang,
resources: props.pageProps.__resources,
});
return i18next;
}, [props.pageProps.__lang, props.pageProps.__resources]);
return (
<I18nextProvider i18n={li18n}>

View File

@ -114,7 +114,6 @@ const GlobalCssTheme = () => {
const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
if (typeof window === "undefined") return children;
// eslint-disable-next-line react-hooks/rules-of-hooks
const registry = useStyleRegistry();
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
};

View File

@ -24,10 +24,18 @@
"style": {
"noNonNullAssertion": "off",
"useImportType": "off",
"noParameterAssign": "off"
"noParameterAssign": "off",
"useEnumInitializers": "off"
},
"suspicious": {
"noExplicitAny": "off"
"noExplicitAny": "off",
"noArrayIndexKey": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"complexity": {
"noBannedTypes": "off"
}
},
"ignore": [

View File

@ -10,13 +10,10 @@
"build:mobile:apk": "yarn workspace mobile build:apk",
"build:mobile:dev": "yarn workspace mobile build:dev",
"update": "yarn workspace mobile update",
"lint": "eslint .",
"format": "prettier -c .",
"format:fix": "prettier -w ."
"lint": "biome lint .",
"format": "biome format .",
"format:fix": "biome format . --write"
},
"eslintIgnore": [
"next-env.d.ts"
],
"workspaces": [
"apps/*",
"packages/*"

View File

@ -49,8 +49,8 @@ export const setCookie = (key: string, val?: unknown) => {
const d = new Date();
// A year
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
const expires = value ? "expires=" + d.toUTCString() : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
document.cookie = key + "=" + value + ";" + expires + ";path=/;samesite=strict";
const expires = value ? `expires=${d.toUTCString()}` : "expires=Thu, 01 Jan 1970 00:00:01 GMT";
document.cookie = `${key}=${value};${expires};path=/;samesite=strict`;
return null;
};
@ -65,10 +65,10 @@ export const readCookie = <T extends ZodTypeAny>(
const ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
while (c.charAt(0) === " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
if (c.indexOf(name) === 0) {
const str = c.substring(name.length, c.length);
return parser ? parser.parse(JSON.parse(str)) : str;
}
@ -85,7 +85,7 @@ export const addAccount = (account: Account) => {
const accounts = readAccounts();
// 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);
return;
}
@ -106,7 +106,7 @@ export const removeAccounts = (filter: (acc: Account) => boolean) => {
export const updateAccount = (id: string, account: Account) => {
const accounts = readAccounts();
const idx = accounts.findIndex((x) => x.id == id);
const idx = accounts.findIndex((x) => x.id === id);
if (idx === -1) return;
const selected = account.selected;

View File

@ -72,7 +72,6 @@ export const ConnectionErrorContext = createContext<{
setError: (error: KyooErrors) => void;
}>({ error: null, loading: true, setError: () => {} });
/* eslint-disable react-hooks/rules-of-hooks */
export const AccountProvider = ({
children,
ssrAccount,
@ -115,7 +114,7 @@ export const AccountProvider = ({
acc?.map((account) => ({
...account,
select: () => updateAccount(account.id, { ...account, selected: true }),
remove: () => removeAccounts((x) => x.id == x.id),
remove: () => removeAccounts((x) => x.id === account.id),
})) ?? [],
[acc],
);
@ -151,6 +150,7 @@ export const AccountProvider = ({
useEffect(() => {
// if the user change account (or connect/disconnect), reset query cache.
if (
// biome-ignore lint/suspicious/noDoubleEquals: id can be an id, null or undefined
selected?.id != oldSelected.current?.id ||
(userIsError && selected?.token.access_token !== oldSelected.current?.token)
) {

View File

@ -94,7 +94,7 @@ let running: ReturnType<typeof getTokenWJ> | null = null;
export const getTokenWJ = async (
acc?: Account | null,
forceRefresh: boolean = false,
forceRefresh = false,
): Promise<readonly [string, Token, null] | readonly [null, null, KyooErrors | null]> => {
if (acc === undefined) acc = getCurrentAccount();
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,
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Refresh token when account change
useEffect(() => {
async function run() {
const nToken = await getTokenWJ();

View File

@ -66,7 +66,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
.join("/")
.replace("//", "/")
.replace("/?", "?");
let resp;
let resp: Response;
try {
resp = await fetch(path, {
method: context.method,
@ -97,11 +97,11 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
if (resp.status === 403 && iToken === undefined && token) {
const [newToken, _, error] = await getTokenWJ(undefined, true);
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) {
const error = await resp.text();
let data;
let data: Record<string, any>;
try {
data = JSON.parse(error);
} 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;
let data;
let data: Record<string, any>;
try {
data = await resp.json();
} catch (e) {
@ -204,11 +204,10 @@ export const toQueryKey = (query: {
query.options?.apiUrl,
...query.path,
query.params
? "?" +
Object.entries(query.params)
? `?${Object.entries(query.params)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
.join("&")
.join("&")}`
: null,
].filter((x) => x);
};
@ -267,12 +266,11 @@ export const fetchQuery = async (queries: QueryIdentifier[], authToken?: string
queryFn: (ctx) => queryFn(ctx, Paged(query.parser), authToken),
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;

View File

@ -196,8 +196,9 @@ export const WatchInfoP = z
// from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-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.
// biome-ignore lint: same as above
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
};

View File

@ -24,7 +24,6 @@ import { Platform } from "react-native";
export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
if (Platform.OS === "web" && typeof window === "undefined" && ssrTheme) return ssrTheme;
// eslint-disable-next-line react-hooks/rules-of-hooks
const [value] = useMMKVString("theme", storage);
if (!value) return "auto";
return value as "light" | "dark" | "auto";

View File

@ -38,7 +38,8 @@ export const getDisplayDate = (data: Show | Movie) => {
return startAir.getFullYear().toString();
}
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
} else if (airDate) {
}
if (airDate) {
return airDate.getFullYear().toString();
}
};

View File

@ -23,6 +23,7 @@
import { type AlertButton, type AlertOptions } from "react-native";
import Swal, { type SweetAlertIcon } from "sweetalert2";
// biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn
export class Alert {
static alert(
title: string,

View File

@ -27,14 +27,13 @@ import { ComponentType, forwardRef, RefAttributes } from "react";
const stringToColor = (string: string) => {
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);
}
let color = "#";
for (i = 0; i < 3; i += 1) {
for (let i = 0; i < 3; i += 1) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}

View File

@ -49,7 +49,7 @@ export const Chip = ({
textProps ??= {};
const sizeMult = size == "medium" ? 1 : size == "small" ? 0.5 : 1.5;
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
return (
<Link

View File

@ -33,7 +33,7 @@ export type Props = {
src?: KyooImage | null;
quality: "low" | "medium" | "high";
alt?: string;
Error?: ReactElement | null;
Err?: ReactElement | null;
forcedLoading?: boolean;
};

View File

@ -22,17 +22,14 @@ import { decode } from "blurhash";
import {
HTMLAttributes,
ReactElement,
createElement,
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import { useYoshiki } from "yoshiki";
import { Stylable, nativeStyleToCss } from "yoshiki/native";
import { StyleList, processStyleList } from "yoshiki/src/type";
import { nativeStyleToCss } from "yoshiki/native";
// The blurhashToUrl has been stolen from https://gist.github.com/mattiaz9/53cb67040fa135cb395b1d015a200aff
export function blurHashToDataURL(hash: string | undefined): string | undefined {
@ -53,7 +50,7 @@ function parsePixels(pixels: Uint8ClampedArray, width: number, height: number) {
typeof Buffer !== "undefined"
? Buffer.from(getPngArray(pngString)).toString("base64")
: btoa(pngString);
return "data:image/png;base64," + dataURL;
return `data:image/png;base64,${dataURL}`;
}
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 NO_FILTER = String.fromCharCode(0);
// biome-ignore lint: not gonna fix stackowerflow code that works
let n, c, k;
// make crc table
@ -89,7 +87,9 @@ function generatePng(width: number, height: number, rgbaString: string) {
function inflateStore(data: string) {
const MAX_STORE_LENGTH = 65535;
let storeBuffer = "";
// biome-ignore lint: not gonna fix stackowerflow code that works
let remaining;
// biome-ignore lint: not gonna fix stackowerflow code that works
let blockType;
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) {
let MOD_ADLER = 65521;
const MOD_ADLER = 65521;
let a = 1;
let b = 0;
@ -179,7 +179,7 @@ function generatePng(width: number, height: number, rgbaString: string) {
const IHDR = createIHDR(width, height);
let scanlines = "";
let scanline;
let scanline: string;
for (let y = 0; y < rgbaString.length; y += width * 4) {
scanline = NO_FILTER;

View File

@ -33,7 +33,7 @@ export const Image = ({
alt,
forcedLoading = false,
layout,
Error,
Err,
...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki();
@ -53,8 +53,8 @@ export const Image = ({
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored") {
return Error !== undefined ? (
Error
return Err !== undefined ? (
Err
) : (
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
);

View File

@ -33,7 +33,7 @@ export const Image = ({
alt,
forcedLoading = false,
layout,
Error,
Err,
...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki();
@ -45,8 +45,8 @@ export const Image = ({
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored") {
return Error !== undefined ? (
Error
return Err !== undefined ? (
Err
) : (
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
);

View File

@ -111,7 +111,7 @@ export const ImageBackground = <AsProps = ViewProps,>({
forcedLoading={forcedLoading}
alt={alt!}
layout={{ width: percent(100), height: percent(100) }}
Error={hideLoad ? null : undefined}
Err={hideLoad ? null : undefined}
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
style: ImageStyle;
})}

View File

@ -22,7 +22,7 @@ import { forwardRef, ReactNode } from "react";
import { Platform, Pressable, TextProps, View, PressableProps, Linking } from "react-native";
import { TextLink, useLink } from "solito/link";
import { useTheme, useYoshiki } from "yoshiki/native";
import type { UrlObject } from "url";
import type { UrlObject } from "node:url";
import { alpha } from "./themes";
import { parseNextPath } from "solito/router";

View File

@ -66,7 +66,6 @@ const Menu = <AsProps,>({
const insets = useSafeAreaInsets();
const alreadyRendered = useRef(false);
const [isOpen, setOpen] =
// eslint-disable-next-line react-hooks/rules-of-hooks
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
// deos the same as a useMemo but for props.

View File

@ -111,9 +111,7 @@ const selectMode = (
};
}
// eslint-disable-next-line react-hooks/rules-of-hooks
const auto = useAutomaticTheme("theme", { light, dark });
// eslint-disable-next-line react-hooks/rules-of-hooks
const alternate = useAutomaticTheme("alternate", { dark: light, light: dark });
return {
...options,

View File

@ -31,7 +31,7 @@ export const Head = ({
}) => {
return (
<NextHead>
{title && <title>{title + " - Kyoo"}</title>}
{title && <title>{`${title} - Kyoo`}</title>}
{description && <meta name="description" content={description} />}
{image && <meta property="og:image" content={image} />}
</NextHead>

View File

@ -82,7 +82,7 @@ export const Scanner = () => {
</View>
))
)}
{data != null && data.length == 0 && <P>{t("admin.scanner.empty")}</P>}
{data != null && data.length === 0 && <P>{t("admin.scanner.empty")}</P>}
</>
</SettingsContainer>
);

View File

@ -121,14 +121,14 @@ export const BrowseSettings = ({
<IconButton
icon={GridView}
onPress={() => setLayout(Layout.Grid)}
color={layout == Layout.Grid ? theme.accent : undefined}
color={layout === Layout.Grid ? theme.accent : undefined}
{...tooltip(t("browse.switchToGrid"))}
{...css({ padding: ts(0.5), marginY: "auto" })}
/>
<IconButton
icon={ViewList}
onPress={() => setLayout(Layout.List)}
color={layout == Layout.List ? theme.accent : undefined}
color={layout === Layout.List ? theme.accent : undefined}
{...tooltip(t("browse.switchToList"))}
{...css({ padding: ts(0.5), marginY: "auto" })}
/>

View File

@ -35,15 +35,14 @@ const MediaInfoTable = ({
const { css } = useYoshiki();
const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`;
const formatTrackTable = (trackTable: (Audio | Subtitle)[], s: string) => {
if (trackTable.length == 0) {
if (trackTable.length === 0) {
return undefined;
}
const singleTrack = trackTable.length == 1;
const singleTrack = trackTable.length === 1;
return trackTable.reduce(
(collected, audioTrack, index) => ({
...collected,
(collected, audioTrack, 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,
// Only show it if there is more than one track
audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
@ -51,8 +50,9 @@ const MediaInfoTable = ({
audioTrack.codec,
]
.filter((x) => x !== undefined)
.join(" - "),
}),
.join(" - ");
return collected;
},
{} as Record<string, string | undefined>,
);
};
@ -94,7 +94,7 @@ const MediaInfoTable = ({
<Skeleton>{value ? <P>{value}</P> : undefined}</Skeleton>
</View>
</View>
{index == l.length - 1 && <HR />}
{index === l.length - 1 && <HR />}
</Fragment>
)),
)}

View File

@ -295,6 +295,7 @@ export const EpisodeLine = ({
>
<Skeleton>
{isLoading || (
// biome-ignore lint/a11y/useValidAriaValues: simply use H6 for the style but keep a P
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
{[displayNumber, name ?? t("show.episodeNoMetadata")].join(" · ")}
</H6>

View File

@ -42,7 +42,7 @@ import { KyooImage } from "@kyoo/models";
import { Atom, useAtomValue } from "jotai";
import DownloadForOffline from "@material-symbols/svg-400/rounded/download_for_offline.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 { useRouter } from "expo-router";
@ -126,6 +126,7 @@ const DownloadedItem = ({
})}
>
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
{/* biome-ignore lint/a11y/useValidAriaValues: use h6 for style only */}
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
{name ?? t("show.episodeNoMetadata")}
</H6>
@ -193,10 +194,9 @@ const downloadIcon = (status: State["status"]) => {
case "DOWNLOADING":
return Downloading;
case "FAILED":
return Error;
return ErrorIcon;
case "PAUSED":
case "STOPPED":
default:
return NotStarted;
}
};

View File

@ -38,7 +38,7 @@ import { Player } from "../player";
import { atom, useSetAtom, PrimitiveAtom, useStore } from "jotai";
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
import { ReactNode, useEffect } from "react";
import { Platform, ToastAndroid } from "react-native";
import { ToastAndroid } from "react-native";
import { QueryClient, useQueryClient } from "@tanstack/react-query";
import { Router } from "expo-router/build/types";
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 }[] =
JSON.parse(storage.getString("downloads") ?? "[]");
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);
const stateAtom = atom({

View File

@ -35,7 +35,7 @@ export const ConnectionError = () => {
const { error, retry } = useContext(ConnectionErrorContext);
const account = useAccount();
if (error && (error.status === 401 || error.status == 403)) {
if (error && (error.status === 401 || error.status === 403)) {
if (!account) {
return (
<View

View File

@ -36,7 +36,7 @@ export const ErrorView = ({
useLayoutEffect(() => {
// 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]);
console.log(error);
return (

View File

@ -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
// 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(() => {
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)
gridGap: layout.gap as any,
},
layout.layout == "vertical" && {
layout.layout === "vertical" && {
gridTemplateColumns: "1fr",
alignItems: "stretch",
overflowY: "auto",
paddingY: layout.gap as any,
},
layout.layout == "horizontal" && {
layout.layout === "horizontal" && {
alignItems: "stretch",
overflowX: "auto",
overflowY: "hidden",

View File

@ -29,9 +29,9 @@ import { DefaultLayout } from "../layout";
export const cleanApiUrl = (apiUrl: string) => {
if (Platform.OS === "web") return undefined;
if (!/https?:\/\//.test(apiUrl)) apiUrl = "http://" + apiUrl;
if (!/https?:\/\//.test(apiUrl)) apiUrl = `http://${apiUrl}`;
apiUrl = apiUrl.replace(/\/$/, "");
return apiUrl + "/api";
return `${apiUrl}/api`;
};
const query: QueryIdentifier<ServerInfo> = {

View File

@ -168,7 +168,7 @@ const VolumeSlider = () => {
>
<IconButton
icon={
isMuted || volume == 0
isMuted || volume === 0
? VolumeOff
: volume < 25
? VolumeMute
@ -204,7 +204,12 @@ const ProgressText = (props: Stylable) => {
export const toTimerString = (timer?: number, duration?: number) => {
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 "??:??";
const h = Math.floor(timer / 3600);
const min = Math.floor((timer / 60) % 60);

View File

@ -41,7 +41,12 @@ type Thumb = {
const parseTs = (time: string) => {
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) => {
@ -64,7 +69,7 @@ export const useScrubber = (url: string) => {
for (let i = 0; i < ret.length; i++) {
const times = lines[i * 2].split(" --> ");
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] = {
from: parseTs(times[0]),
to: parseTs(times[1]),
@ -164,7 +169,7 @@ export const BottomScrubber = ({ url, chapters }: { url: string; chapters?: Chap
<View {...css({ overflow: "hidden" })}>
<View
{...(Platform.OS === "web"
? css({ transform: `translateX(50%)` })
? css({ transform: "translateX(50%)" })
: {
// react-native does not support translateX by percentage so we simulate it
style: { transform: [{ translateX: scrubberWidth / 2 }] },

View File

@ -124,13 +124,11 @@ export const Player = ({
title={
data.type === "movie"
? data.name
: data.show!.name +
" " +
episodeDisplayNumber({
: `${data.show!.name} ${episodeDisplayNumber({
seasonNumber: data.seasonNumber,
episodeNumber: data.episodeNumber,
absoluteNumber: data.absoluteNumber,
})
})}`
}
description={data.overview}
/>

View File

@ -68,7 +68,7 @@ export const reducerAtom = atom(null, (get, set, action: Action) => {
case "volume":
set(volumeAtom, Math.max(0, Math.min(get(volumeAtom) + action.value, 100)));
break;
case "subtitle":
case "subtitle": {
const subtitle = get(subtitleAtom);
const index = subtitle ? action.subtitles.findIndex((x) => x.index === subtitle.index) : -1;
set(
@ -76,6 +76,7 @@ export const reducerAtom = atom(null, (get, set, action: Action) => {
index === -1 ? null : action.subtitles[(index + 1) % action.subtitles.length],
);
break;
}
}
});

View File

@ -143,22 +143,24 @@ export const Video = memo(function Video({
const account = useAccount();
const defaultSubLanguage = account?.settings.subtitleLanguage;
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(() => {
if (!subtitles) return;
setSubtitle((subtitle) => {
const subRet = subtitle ? subtitles.find((x) => x.language === subtitle.language) : null;
if (subRet) return subRet;
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;
});
// 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]);
const defaultAudioLanguage = account?.settings.audioLanguage ?? "default";
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(() => {
if (!audios) return;
setAudio((audio) => {
@ -172,9 +174,6 @@ export const Video = memo(function Video({
}
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]);
const volume = useAtomValue(volumeAtom);
@ -225,7 +224,7 @@ export const Video = memo(function Video({
label: t("player.unsupportedError"),
duration: 3,
});
if (mode == PlayMode.Direct) setPlayMode(PlayMode.Hls);
if (mode === PlayMode.Direct) setPlayMode(PlayMode.Hls);
}}
/>
);

View File

@ -33,9 +33,9 @@ declare module "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 { ComponentProps, forwardRef, useEffect, useRef } from "react";
import { ComponentProps, forwardRef, useEffect, } from "react";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import NativeVideo, {
VideoRef,
@ -160,13 +160,13 @@ export const QualitiesMenu = (props: CustomMenu) => {
<Menu {...props}>
<Menu.Item
label={t("player.direct")}
selected={mode == PlayMode.Direct}
selected={mode === PlayMode.Direct}
onSelect={() => setPlayMode(PlayMode.Direct)}
/>
<Menu.Item
// TODO: Display the currently selected quality (impossible with rn-video right now)
label={t("player.auto")}
selected={video === -1 && mode == PlayMode.Hls}
selected={video === -1 && mode === PlayMode.Hls}
onSelect={() => {
setPlayMode(PlayMode.Hls);
setVideo(-1);

View File

@ -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 => {
if (hls !== null) return hls;
const loadPolicy: LoadPolicy = {
default: {
maxTimeToFirstByteMs: Infinity,
maxTimeToFirstByteMs: Number.POSITIVE_INFINITY,
maxLoadTimeMs: 60_000,
timeoutRetry: {
maxNumRetry: 2,
@ -74,14 +74,14 @@ const initHls = (): Hls => {
xhr.setRequestHeader("X-CLIENT-ID", client_id);
},
autoStartLoad: false,
startLevel: Infinity,
startLevel: Number.POSITIVE_INFINITY,
abrEwmaDefaultEstimate: 35_000_000,
abrEwmaDefaultEstimateMax: 50_000_000,
// debug: true,
lowLatencyMode: false,
fragLoadPolicy: {
default: {
maxTimeToFirstByteMs: Infinity,
maxTimeToFirstByteMs: Number.POSITIVE_INFINITY,
maxLoadTimeMs: 60_000,
timeoutRetry: {
maxNumRetry: 5,
@ -148,6 +148,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
const subtitle = useAtomValue(subtitleAtom);
useSubtitle(ref, subtitle, fonts);
// biome-ignore lint/correctness/useExhaustiveDependencies: onError changes should not restart the playback.
useLayoutEffect(() => {
if (!ref?.current || !source.uri) return;
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]);
const mode = useAtomValue(playModeAtom);
const audio = useAtomValue(audioAtom);
// biome-ignore lint/correctness/useExhaustiveDependencies: also change when the mode change
useEffect(() => {
if (!hls) return;
const update = () => {
@ -389,12 +389,12 @@ export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
<Menu {...props}>
<Menu.Item
label={t("player.direct")}
selected={hls === null || mode == PlayMode.Direct}
selected={hls === null || mode === PlayMode.Direct}
onSelect={() => setPlayMode(PlayMode.Direct)}
/>
<Menu.Item
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")
}

View File

@ -42,7 +42,7 @@ export const WatchStatusObserver = ({
await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }),
});
const mutate = useCallback(
(seconds: number) =>
(type: string, slug: string, seconds: number) =>
_mutate({
method: "POST",
path: [type, slug, "watchStatus"],
@ -52,7 +52,7 @@ export const WatchStatusObserver = ({
percent: Math.round((seconds / duration) * 100),
},
}),
[_mutate, type, slug, duration],
[_mutate, duration],
);
const readProgress = useAtomCallback(
useCallback((get) => {
@ -65,19 +65,20 @@ export const WatchStatusObserver = ({
useEffect(() => {
if (!account) return;
const timer = setInterval(() => {
mutate(readProgress());
mutate(type, slug, readProgress());
}, 10_000);
return () => {
clearInterval(timer);
mutate(readProgress());
mutate(type, slug, readProgress());
};
}, [account, type, slug, readProgress, mutate]);
// update watch status when play status change (and on mount).
const isPlaying = useAtomValue(playAtom);
// biome-ignore lint/correctness/useExhaustiveDependencies: Include isPlaying
useEffect(() => {
if (!account) return;
mutate(readProgress());
mutate(type, slug, readProgress());
}, [account, type, slug, isPlaying, readProgress, mutate]);
return null;
};

View File

@ -44,7 +44,7 @@ const query = (
params: {
q: query,
sortBy:
sortKey && sortKey != SearchSort.Relevance ? `${sortKey}:${sortOrd ?? "asc"}` : undefined,
sortKey && sortKey !== SearchSort.Relevance ? `${sortKey}:${sortOrd ?? "asc"}` : undefined,
},
});

View File

@ -40,7 +40,7 @@ function dataURItoBlob(dataURI: string) {
const byteString = atob(dataURI.split(",")[1]);
const ab = new ArrayBuffer(byteString.length);
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);
}
return new Blob([ab], { type: "image/jpeg" });

View File

@ -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)
// 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",];
export const PlaybackSettings = () => {

File diff suppressed because it is too large Load Diff