diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 25e15b0c..af7e2b84 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -25,18 +25,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 with: - node-version: 18.x - cache: yarn - cache-dependency-path: front/yarn.lock + version: latest - - name: Install dependencies - run: yarn install --immutable - - - name: Lint - run: yarn lint && yarn format + - name: Run Biome + run: biome ci . scanner: name: "Lint scanner/autosync" diff --git a/front/.eslintrc.json b/front/.eslintrc.json deleted file mode 100755 index e7c4afae..00000000 --- a/front/.eslintrc.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["header"], - "settings": { - "next": { - "rootDir": "apps/web/" - } - }, - "rules": { - "@next/next/no-img-element": "off", - "header/header": [ - "error", - "block", - [ - "", - " * Kyoo - A portable and vast media library solution.", - " * Copyright (c) Kyoo.", - " *", - " * See AUTHORS.md and LICENSE file in the project root for full license information.", - " *", - " * Kyoo is free software: you can redistribute it and/or modify", - " * it under the terms of the GNU General Public License as published by", - " * the Free Software Foundation, either version 3 of the License, or", - " * any later version.", - " *", - " * Kyoo is distributed in the hope that it will be useful,", - " * but WITHOUT ANY WARRANTY; without even the implied warranty of", - " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the", - " * GNU General Public License for more details.", - " *", - " * You should have received a copy of the GNU General Public License", - " * along with Kyoo. If not, see .", - " " - ], - 2 - ] - } -} diff --git a/front/.prettierignore b/front/.prettierignore deleted file mode 100644 index 61c3bc75..00000000 --- a/front/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -.yarn diff --git a/front/apps/mobile/app.config.js b/front/apps/mobile/app.config.js index a8ee7317..b0abf42a 100644 --- a/front/apps/mobile/app.config.js +++ b/front/apps/mobile/app.config.js @@ -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", diff --git a/front/apps/mobile/app/(app)/(tabs)/_layout.tsx b/front/apps/mobile/app/(app)/(tabs)/_layout.tsx index 814bb0aa..9f6f1d21 100644 --- a/front/apps/mobile/app/(app)/(tabs)/_layout.tsx +++ b/front/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -19,10 +19,10 @@ */ import { Icon } from "@kyoo/primitives"; -import { Tabs } from "expo-router"; -import Home from "@material-symbols/svg-400/rounded/home-fill.svg"; import Browse from "@material-symbols/svg-400/rounded/browse-fill.svg"; import Downloading from "@material-symbols/svg-400/rounded/downloading-fill.svg"; +import Home from "@material-symbols/svg-400/rounded/home-fill.svg"; +import { Tabs } from "expo-router"; export default function TabsLayout() { return ( diff --git a/front/apps/mobile/app/(public)/_layout.tsx b/front/apps/mobile/app/(public)/_layout.tsx index cb81a20c..90dfb3de 100644 --- a/front/apps/mobile/app/(public)/_layout.tsx +++ b/front/apps/mobile/app/(public)/_layout.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { Account, ConnectionErrorContext, useAccount } from "@kyoo/models"; +import { type Account, ConnectionErrorContext, useAccount } from "@kyoo/models"; import { NavbarProfile, NavbarTitle } from "@kyoo/ui"; import { Redirect, Stack } from "expo-router"; import { useContext, useRef } from "react"; @@ -30,7 +30,7 @@ export default function PublicLayout() { const { error } = useContext(ConnectionErrorContext); const oldAccount = useRef(account); - if (account && !error && account != oldAccount.current) return ; + if (account && !error && account !== oldAccount.current) return ; oldAccount.current = account; return ( diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index 304d291e..e43a8d28 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -20,39 +20,39 @@ import "react-native-reanimated"; -import { PortalProvider } from "@gorhom/portal"; -import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives"; -import { DownloadProvider } from "@kyoo/ui"; -import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models"; -import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; -import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; -import i18next from "i18next"; -import { Slot } from "expo-router"; -import { getLocales } from "expo-localization"; -import * as SplashScreen from "expo-splash-screen"; import { - useFonts, Poppins_300Light, Poppins_400Regular, Poppins_900Black, + useFonts, } from "@expo-google-fonts/poppins"; -import { ReactNode, useEffect, useState } from "react"; -import { useColorScheme } from "react-native"; -import { initReactI18next } from "react-i18next"; -import { ThemeProvider as RNThemeProvider } from "@react-navigation/native"; -import "intl-pluralrules"; -import "@formatjs/intl-locale/polyfill"; -import "@formatjs/intl-displaynames/polyfill"; import "@formatjs/intl-displaynames/locale-data/en"; import "@formatjs/intl-displaynames/locale-data/fr"; +import "@formatjs/intl-displaynames/polyfill"; +import "@formatjs/intl-locale/polyfill"; +import { PortalProvider } from "@gorhom/portal"; +import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models"; +import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives"; +import { DownloadProvider } from "@kyoo/ui"; +import { ThemeProvider as RNThemeProvider } from "@react-navigation/native"; +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { getLocales } from "expo-localization"; +import { Slot } from "expo-router"; +import * as SplashScreen from "expo-splash-screen"; +import i18next from "i18next"; +import "intl-pluralrules"; +import { type ReactNode, useEffect, useState } from "react"; +import { initReactI18next } from "react-i18next"; +import { useColorScheme } from "react-native"; +import NetInfo from "@react-native-community/netinfo"; +import { onlineManager } from "@tanstack/react-query"; +import { useTheme } from "yoshiki/native"; // TODO: use a backend to load jsons. import en from "../../../translations/en.json"; import fr from "../../../translations/fr.json"; import zh from "../../../translations/zh.json"; -import { useTheme } from "yoshiki/native"; -import NetInfo from "@react-native-community/netinfo"; -import { onlineManager } from "@tanstack/react-query"; onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { diff --git a/front/apps/mobile/app/utils.tsx b/front/apps/mobile/app/utils.tsx index d63e4332..85725f30 100644 --- a/front/apps/mobile/app/utils.tsx +++ b/front/apps/mobile/app/utils.tsx @@ -18,14 +18,14 @@ * along with Kyoo. If not, see . */ -import { Stack, useLocalSearchParams } from "expo-router"; -import { ComponentType, useEffect } from "react"; -import { StatusBar, StatusBarProps } from "react-native"; -import * as ScreenOrientation from "expo-screen-orientation"; -import * as NavigationBar from "expo-navigation-bar"; -import arrayShuffle from "array-shuffle"; -import { QueryPage, useHasPermission } from "@kyoo/models"; +import { type QueryPage, useHasPermission } from "@kyoo/models"; import { Unauthorized } from "@kyoo/ui"; +import arrayShuffle from "array-shuffle"; +import * as NavigationBar from "expo-navigation-bar"; +import { Stack, useLocalSearchParams } from "expo-router"; +import * as ScreenOrientation from "expo-screen-orientation"; +import { type ComponentType, useEffect } from "react"; +import { StatusBar, type StatusBarProps } from "react-native"; const FullscreenProvider = () => { useEffect(() => { diff --git a/front/apps/mobile/metro.config.js b/front/apps/mobile/metro.config.js index e1528c71..9ba7f3d0 100644 --- a/front/apps/mobile/metro.config.js +++ b/front/apps/mobile/metro.config.js @@ -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); diff --git a/front/apps/mobile/tsconfig.json b/front/apps/mobile/tsconfig.json index be0953e7..c4a1038b 100644 --- a/front/apps/mobile/tsconfig.json +++ b/front/apps/mobile/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { - "strict": true, - }, + "strict": true + } } diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js index 366daaeb..a97614aa 100755 --- a/front/apps/web/next.config.js +++ b/front/apps/web/next.config.js @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -const path = require("path"); +const path = require("node:path"); const CopyPlugin = require("copy-webpack-plugin"); const DefinePlugin = require("webpack").DefinePlugin; diff --git a/front/apps/web/package.json b/front/apps/web/package.json index acd29d76..8388d969 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -2,9 +2,7 @@ "name": "web", "version": "0.1.0", "private": true, - "sideEffects": [ - "./src/polyfill.ts" - ], + "sideEffects": ["./src/polyfill.ts"], "scripts": { "dev": "next dev", "build": "next build", @@ -56,8 +54,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" diff --git a/front/apps/web/src/i18n-d.ts b/front/apps/web/src/i18n-d.ts index eeee0be0..46c3f118 100644 --- a/front/apps/web/src/i18n-d.ts +++ b/front/apps/web/src/i18n-d.ts @@ -19,7 +19,7 @@ */ import "i18next"; -import en from "../../../translations/en.json"; +import type en from "../../../translations/en.json"; declare module "i18next" { interface CustomTypeOptions { diff --git a/front/apps/web/src/i18n.tsx b/front/apps/web/src/i18n.tsx index fb144e0a..418e6604 100644 --- a/front/apps/web/src/i18n.tsx +++ b/front/apps/web/src/i18n.tsx @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ -import { ComponentType, useMemo } from "react"; -import i18next, { InitOptions } from "i18next"; +import i18next, { type InitOptions } from "i18next"; +import type { AppContext, AppInitialProps, AppProps } from "next/app"; +import { type ComponentType, useMemo } from "react"; import { I18nextProvider } from "react-i18next"; -import { AppContext, AppInitialProps, type AppProps } from "next/app"; import en from "../../../translations/en.json"; import fr from "../../../translations/fr.json"; @@ -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 ( diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index 1744cd7c..7b3c156c 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -20,42 +20,42 @@ import "../polyfill"; -import { HydrationBoundary, QueryClientProvider, dehydrate } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { - HiddenIfNoJs, - TouchOnlyCss, - SkeletonCss, - ThemeSelector, - SnackbarProvider, -} from "@kyoo/primitives"; -import { WebTooltip } from "@kyoo/primitives/src/tooltip.web"; +import { PortalProvider } from "@gorhom/portal"; import { AccountP, AccountProvider, ConnectionErrorContext, + type QueryIdentifier, + type QueryPage, + ServerInfoP, + UserP, createQueryClient, fetchQuery, getTokenWJ, - QueryIdentifier, - QueryPage, - ServerInfoP, setSsrApiUrl, - UserP, useUserTheme, } from "@kyoo/models"; -import { ComponentType, useContext, useState } from "react"; -import NextApp, { AppContext, type AppProps } from "next/app"; -import { Poppins } from "next/font/google"; -import { useTheme, useMobileHover, useStyleRegistry, StyleRegistryProvider } from "yoshiki/web"; -import superjson from "superjson"; -import Head from "next/head"; -import { withTranslations } from "../i18n"; -import arrayShuffle from "array-shuffle"; -import { Tooltip } from "react-tooltip"; import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal"; -import { PortalProvider } from "@gorhom/portal"; +import { + HiddenIfNoJs, + SkeletonCss, + SnackbarProvider, + ThemeSelector, + TouchOnlyCss, +} from "@kyoo/primitives"; +import { WebTooltip } from "@kyoo/primitives/src/tooltip.web"; import { ConnectionError } from "@kyoo/ui"; +import { HydrationBoundary, QueryClientProvider, dehydrate } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import arrayShuffle from "array-shuffle"; +import NextApp, { type AppContext, type AppProps } from "next/app"; +import { Poppins } from "next/font/google"; +import Head from "next/head"; +import { type ComponentType, useContext, useState } from "react"; +import { Tooltip } from "react-tooltip"; +import superjson from "superjson"; +import { StyleRegistryProvider, useMobileHover, useStyleRegistry, useTheme } from "yoshiki/web"; +import { withTranslations } from "../i18n"; const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" }); @@ -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 {children}; }; diff --git a/front/apps/web/src/pages/_document.tsx b/front/apps/web/src/pages/_document.tsx index 68ef3fc9..7583f1dd 100644 --- a/front/apps/web/src/pages/_document.tsx +++ b/front/apps/web/src/pages/_document.tsx @@ -18,9 +18,9 @@ * along with Kyoo. If not, see . */ +import { type DocumentContext, Head, Html, Main, NextScript } from "next/document"; import { AppRegistry } from "react-native"; -import { Html, Main, Head, NextScript, DocumentContext } from "next/document"; -import { createStyleRegistry, StyleRegistryProvider } from "yoshiki/web"; +import { StyleRegistryProvider, createStyleRegistry } from "yoshiki/web"; export const style = ` /** diff --git a/front/apps/web/src/router.tsx b/front/apps/web/src/router.tsx index 63642990..dee9eb60 100644 --- a/front/apps/web/src/router.tsx +++ b/front/apps/web/src/router.tsx @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ -import { QueryPage, useHasPermission } from "@kyoo/models"; +import { type QueryPage, useHasPermission } from "@kyoo/models"; import { Unauthorized } from "@kyoo/ui"; import { useRouter } from "next/router"; -import { ComponentType } from "react"; +import type { ComponentType } from "react"; export const withRoute = ( Component: ComponentType, diff --git a/front/apps/web/tsconfig.json b/front/apps/web/tsconfig.json index ce0fd788..0cd6e2a1 100755 --- a/front/apps/web/tsconfig.json +++ b/front/apps/web/tsconfig.json @@ -16,9 +16,9 @@ "incremental": true, "baseUrl": ".", "paths": { - "~/*": ["src/*"], - }, + "~/*": ["src/*"] + } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], + "exclude": ["node_modules"] } diff --git a/front/biome.json b/front/biome.json new file mode 100644 index 00000000..5dda7011 --- /dev/null +++ b/front/biome.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "tab", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto", + "ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts"] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "off", + "useEnumInitializers": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noArrayIndexKey": "off" + }, + "security": { + "noDangerouslySetInnerHtml": "off" + }, + "complexity": { + "noBannedTypes": "off" + } + }, + "ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts"] + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingComma": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "quoteStyle": "double", + "attributePosition": "auto" + } + } +} diff --git a/front/package.json b/front/package.json index af68b360..346c815b 100644 --- a/front/package.json +++ b/front/package.json @@ -10,28 +10,13 @@ "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 ." - }, - "eslintIgnore": [ - "next-env.d.ts" - ], - "workspaces": [ - "apps/*", - "packages/*" - ], - "prettier": { - "useTabs": true, - "printWidth": 100, - "trailingComma": "all" + "lint": "biome lint .", + "format": "biome format .", + "format:fix": "biome format . --write" }, + "workspaces": ["apps/*", "packages/*"], "devDependencies": { - "eslint": "8.56.0", - "eslint-config-next": "14.1.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-header": "^3.1.1", - "prettier": "^3.2.4", + "@biomejs/biome": "1.7.3", "typescript": "5.3.3" }, "packageManager": "yarn@3.2.4" diff --git a/front/packages/models/src/account-internal.ts b/front/packages/models/src/account-internal.ts index c8a859c1..6d5ca73e 100644 --- a/front/packages/models/src/account-internal.ts +++ b/front/packages/models/src/account-internal.ts @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ -import { ZodTypeAny, z } from "zod"; -import { Account, AccountP } from "./accounts"; -import { MMKV } from "react-native-mmkv"; import { Platform } from "react-native"; +import { MMKV } from "react-native-mmkv"; +import { type ZodTypeAny, z } from "zod"; +import { type Account, AccountP } from "./accounts"; export const storage = new MMKV(); @@ -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 = ( 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; diff --git a/front/packages/models/src/accounts.tsx b/front/packages/models/src/accounts.tsx index 668710f9..5dbb98d4 100644 --- a/front/packages/models/src/accounts.tsx +++ b/front/packages/models/src/accounts.tsx @@ -18,17 +18,25 @@ * along with Kyoo. If not, see . */ -import { ReactNode, createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { ServerInfoP, User, UserP } from "./resources"; -import { z } from "zod"; -import { zdate } from "./utils"; -import { removeAccounts, setCookie, updateAccount } from "./account-internal"; -import { useMMKVString } from "react-native-mmkv"; -import { Platform } from "react-native"; import { useQueryClient } from "@tanstack/react-query"; import { atom, getDefaultStore, useAtomValue, useSetAtom } from "jotai"; +import { + type ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Platform } from "react-native"; +import { useMMKVString } from "react-native-mmkv"; +import { z } from "zod"; +import { removeAccounts, setCookie, updateAccount } from "./account-internal"; +import type { KyooErrors } from "./kyoo-errors"; import { useFetch } from "./query"; -import { KyooErrors } from "./kyoo-errors"; +import { ServerInfoP, type User, UserP } from "./resources"; +import { zdate } from "./utils"; export const TokenP = z.object({ token_type: z.literal("Bearer"), @@ -72,7 +80,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 +122,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 +158,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) ) { diff --git a/front/packages/models/src/login.ts b/front/packages/models/src/login.ts index e9799a12..e846d74d 100644 --- a/front/packages/models/src/login.ts +++ b/front/packages/models/src/login.ts @@ -18,13 +18,13 @@ * along with Kyoo. If not, see . */ -import { queryFn } from "./query"; -import { KyooErrors } from "./kyoo-errors"; -import { Account, Token, TokenP, getCurrentApiUrl } from "./accounts"; -import { UserP } from "./resources"; -import { addAccount, getCurrentAccount, removeAccounts, updateAccount } from "./account-internal"; -import { Platform } from "react-native"; import { useEffect, useRef, useState } from "react"; +import { Platform } from "react-native"; +import { addAccount, getCurrentAccount, removeAccounts, updateAccount } from "./account-internal"; +import { type Account, type Token, TokenP, getCurrentApiUrl } from "./accounts"; +import type { KyooErrors } from "./kyoo-errors"; +import { queryFn } from "./query"; +import { UserP } from "./resources"; type Result = | { ok: true; value: A; error?: undefined } @@ -94,7 +94,7 @@ let running: ReturnType | null = null; export const getTokenWJ = async ( acc?: Account | null, - forceRefresh: boolean = false, + forceRefresh = false, ): Promise => { 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(); diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index e7311a0b..c39978c6 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -18,18 +18,18 @@ * along with Kyoo. If not, see . */ -import { ComponentType, ReactElement } from "react"; import { QueryClient, - QueryFunctionContext, + type QueryFunctionContext, useInfiniteQuery, useQuery, } from "@tanstack/react-query"; -import { z } from "zod"; -import { KyooErrors } from "./kyoo-errors"; -import { Page, Paged } from "./page"; -import { getToken, getTokenWJ } from "./login"; +import type { ComponentType, ReactElement } from "react"; +import type { z } from "zod"; import { getCurrentApiUrl } from "."; +import type { KyooErrors } from "./kyoo-errors"; +import { getToken, getTokenWJ } from "./login"; +import { type Page, Paged } from "./page"; export let lastUsedUrl: string = null!; @@ -66,7 +66,7 @@ export const queryFn = async ( .join("/") .replace("//", "/") .replace("/?", "?"); - let resp; + let resp: Response; try { resp = await fetch(path, { method: context.method, @@ -97,11 +97,11 @@ export const queryFn = async ( 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; try { data = JSON.parse(error); } catch (e) { @@ -122,7 +122,7 @@ export const queryFn = async ( if ("plainText" in context && context.plainText) return (await resp.text()) as unknown; - let data; + let data: Record; 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; diff --git a/front/packages/models/src/resources/episode.base.ts b/front/packages/models/src/resources/episode.base.ts index 4908c773..aa9280c6 100644 --- a/front/packages/models/src/resources/episode.base.ts +++ b/front/packages/models/src/resources/episode.base.ts @@ -19,9 +19,9 @@ */ import { z } from "zod"; -import { zdate } from "../utils"; import { ImagesP, imageFn } from "../traits"; import { ResourceP } from "../traits/resource"; +import { zdate } from "../utils"; export const BaseEpisodeP = ResourceP("episode") .merge(ImagesP) diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index 5aa19dde..bac4ee07 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -19,8 +19,8 @@ */ import { z } from "zod"; -import { ShowP } from "./show"; import { BaseEpisodeP } from "./episode.base"; +import { ShowP } from "./show"; import { WatchStatusP } from "./watch-status"; export const EpisodeP = BaseEpisodeP.and( diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts index 7554df1a..74108101 100644 --- a/front/packages/models/src/resources/movie.ts +++ b/front/packages/models/src/resources/movie.ts @@ -19,13 +19,13 @@ */ import { z } from "zod"; -import { zdate } from "../utils"; import { ImagesP, ResourceP, imageFn } from "../traits"; -import { Genre } from "./genre"; -import { StudioP } from "./studio"; -import { Status } from "./show"; +import { zdate } from "../utils"; import { CollectionP } from "./collection"; +import { Genre } from "./genre"; import { MetadataP } from "./metadata"; +import { Status } from "./show"; +import { StudioP } from "./studio"; import { WatchStatusP } from "./watch-status"; export const MovieP = ResourceP("movie") diff --git a/front/packages/models/src/resources/news.ts b/front/packages/models/src/resources/news.ts index 30f724a7..0e04ab27 100644 --- a/front/packages/models/src/resources/news.ts +++ b/front/packages/models/src/resources/news.ts @@ -19,8 +19,8 @@ */ import { z } from "zod"; -import { MovieP } from "./movie"; import { EpisodeP } from "./episode"; +import { MovieP } from "./movie"; export const NewsP = z.union([ /* diff --git a/front/packages/models/src/resources/season.ts b/front/packages/models/src/resources/season.ts index 9c5c1fa2..6229d56d 100644 --- a/front/packages/models/src/resources/season.ts +++ b/front/packages/models/src/resources/season.ts @@ -19,8 +19,8 @@ */ import { z } from "zod"; -import { zdate } from "../utils"; import { ImagesP, ResourceP } from "../traits"; +import { zdate } from "../utils"; export const SeasonP = ResourceP("season").merge(ImagesP).extend({ /** diff --git a/front/packages/models/src/resources/server-info.ts b/front/packages/models/src/resources/server-info.ts index 7266e89d..370ba6d2 100644 --- a/front/packages/models/src/resources/server-info.ts +++ b/front/packages/models/src/resources/server-info.ts @@ -18,9 +18,9 @@ * along with Kyoo. If not, see . */ +import { Platform } from "react-native"; import { z } from "zod"; import { imageFn } from ".."; -import { Platform } from "react-native"; export const OidcInfoP = z.object({ /* diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index 8b8ee32c..c6b52c54 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -19,12 +19,12 @@ */ import { z } from "zod"; -import { zdate } from "../utils"; import { ImagesP, ResourceP } from "../traits"; -import { Genre } from "./genre"; -import { StudioP } from "./studio"; +import { zdate } from "../utils"; import { BaseEpisodeP } from "./episode.base"; +import { Genre } from "./genre"; import { MetadataP } from "./metadata"; +import { StudioP } from "./studio"; import { ShowWatchStatusP } from "./watch-status"; /** diff --git a/front/packages/models/src/resources/user.ts b/front/packages/models/src/resources/user.ts index f17ea5d5..0f087a10 100644 --- a/front/packages/models/src/resources/user.ts +++ b/front/packages/models/src/resources/user.ts @@ -19,8 +19,8 @@ */ import { z } from "zod"; -import { ResourceP } from "../traits/resource"; import { imageFn } from "../traits/images"; +import { ResourceP } from "../traits/resource"; export const UserP = ResourceP("user") .extend({ diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts index de8d01d9..1fd8ce09 100644 --- a/front/packages/models/src/resources/watch-info.ts +++ b/front/packages/models/src/resources/watch-info.ts @@ -18,9 +18,9 @@ * along with Kyoo. If not, see . */ +import i18next from "i18next"; import { z } from "zod"; import { imageFn } from "../traits"; -import i18next from "i18next"; import { QualityP } from "./quality"; const getDisplayName = (sub: Track) => { @@ -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]; }; diff --git a/front/packages/models/src/theme.ts b/front/packages/models/src/theme.ts index 01523423..36ccb119 100644 --- a/front/packages/models/src/theme.ts +++ b/front/packages/models/src/theme.ts @@ -18,13 +18,12 @@ * along with Kyoo. If not, see . */ +import { Platform } from "react-native"; import { useMMKVString } from "react-native-mmkv"; import { setCookie, storage } from "./account-internal"; -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"; diff --git a/front/packages/models/src/utils.ts b/front/packages/models/src/utils.ts index 3ec22907..6d3557de 100644 --- a/front/packages/models/src/utils.ts +++ b/front/packages/models/src/utils.ts @@ -19,10 +19,10 @@ */ import { Platform } from "react-native"; -import { Movie, Show } from "./resources"; -import { z } from "zod"; import { useMMKVString } from "react-native-mmkv"; +import { z } from "zod"; import { storage } from "./account-internal"; +import type { Movie, Show } from "./resources"; export const zdate = z.coerce.date; @@ -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(); } }; diff --git a/front/packages/models/tsconfig.json b/front/packages/models/tsconfig.json index b8a9f496..8376e1ef 100755 --- a/front/packages/models/tsconfig.json +++ b/front/packages/models/tsconfig.json @@ -18,9 +18,9 @@ "incremental": true, "baseUrl": ".", "paths": { - "~/*": ["src/*"], - }, + "~/*": ["src/*"] + } }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], + "exclude": ["node_modules"] } diff --git a/front/packages/primitives/src/alert.tsx b/front/packages/primitives/src/alert.tsx index d87f147e..12e2210e 100644 --- a/front/packages/primitives/src/alert.tsx +++ b/front/packages/primitives/src/alert.tsx @@ -20,8 +20,8 @@ // Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 -import { Alert as RNAlert, type AlertOptions, type AlertButton } from "react-native"; -import { type SweetAlertIcon } from "sweetalert2"; +import { type AlertButton, type AlertOptions, Alert as RNAlert } from "react-native"; +import type { SweetAlertIcon } from "sweetalert2"; export interface ExtendedAlertStatic { alert: ( diff --git a/front/packages/primitives/src/alert.web.tsx b/front/packages/primitives/src/alert.web.tsx index 6ba044e0..84c32ca0 100644 --- a/front/packages/primitives/src/alert.web.tsx +++ b/front/packages/primitives/src/alert.web.tsx @@ -20,9 +20,10 @@ // Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 -import { type AlertButton, type AlertOptions } from "react-native"; +import type { AlertButton, 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, diff --git a/front/packages/primitives/src/avatar.tsx b/front/packages/primitives/src/avatar.tsx index da72377d..a2bcea47 100644 --- a/front/packages/primitives/src/avatar.tsx +++ b/front/packages/primitives/src/avatar.tsx @@ -18,23 +18,22 @@ * along with Kyoo. If not, see . */ -import { View, ViewStyle, Image, ImageProps } from "react-native"; -import { useYoshiki, px, Stylable } from "yoshiki/native"; +import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; +import { type ComponentType, type RefAttributes, forwardRef } from "react"; +import { Image, type ImageProps, View, type ViewStyle } from "react-native"; +import { type Stylable, px, useYoshiki } from "yoshiki/native"; import { Icon } from "./icons"; import { P } from "./text"; -import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; -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); } diff --git a/front/packages/primitives/src/button.tsx b/front/packages/primitives/src/button.tsx index 5f902793..bb63ca10 100644 --- a/front/packages/primitives/src/button.tsx +++ b/front/packages/primitives/src/button.tsx @@ -18,12 +18,12 @@ * along with Kyoo. If not, see . */ -import { ComponentType, ForwardedRef, ReactElement, forwardRef } from "react"; -import { Theme, useYoshiki } from "yoshiki/native"; +import { type ComponentType, type ForwardedRef, type ReactElement, forwardRef } from "react"; +import { type Falsy, type PressableProps, View } from "react-native"; +import { type Theme, useYoshiki } from "yoshiki/native"; import { PressableFeedback } from "./links"; import { P } from "./text"; import { ts } from "./utils"; -import { Falsy, PressableProps, View } from "react-native"; export const Button = forwardRef(function Button( { diff --git a/front/packages/primitives/src/chip.tsx b/front/packages/primitives/src/chip.tsx index 1c757ce8..a4930fb7 100644 --- a/front/packages/primitives/src/chip.tsx +++ b/front/packages/primitives/src/chip.tsx @@ -18,12 +18,12 @@ * along with Kyoo. If not, see . */ -import { px, rem, Theme, useYoshiki } from "yoshiki/native"; +import type { TextProps } from "react-native"; +import { type Theme, px, rem, useYoshiki } from "yoshiki/native"; import { Link } from "./links"; +import { Skeleton } from "./skeleton"; import { P } from "./text"; import { capitalize, ts } from "./utils"; -import { TextProps } from "react-native"; -import { Skeleton } from "./skeleton"; export const Chip = ({ color, @@ -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 ( . */ -import { ComponentType } from "react"; -import { View, ViewProps } from "react-native"; +import type { ComponentType } from "react"; +import { View, type ViewProps } from "react-native"; import { percent, px, useYoshiki } from "yoshiki/native"; -export const Container = ({ +export const Container = ({ as, ...props }: { as?: ComponentType } & AsProps) => { diff --git a/front/packages/primitives/src/divider.tsx b/front/packages/primitives/src/divider.tsx index 8b37cdac..03176ce7 100644 --- a/front/packages/primitives/src/divider.tsx +++ b/front/packages/primitives/src/divider.tsx @@ -19,7 +19,7 @@ */ import { HR as EHR } from "@expo/html-elements"; -import { px, Stylable, useYoshiki } from "yoshiki/native"; +import { type Stylable, px, useYoshiki } from "yoshiki/native"; import { ts } from "./utils"; export const HR = ({ diff --git a/front/packages/primitives/src/icons.tsx b/front/packages/primitives/src/icons.tsx index a8cd8791..3c504c33 100644 --- a/front/packages/primitives/src/icons.tsx +++ b/front/packages/primitives/src/icons.tsx @@ -18,14 +18,15 @@ * along with Kyoo. If not, see . */ -import React, { ComponentProps, ComponentType, ForwardedRef, forwardRef } from "react"; -import { Platform, PressableProps } from "react-native"; -import { SvgProps } from "react-native-svg"; -import { YoshikiStyle } from "yoshiki"; -import { px, Stylable, Theme, useYoshiki } from "yoshiki/native"; +import type React from "react"; +import { type ComponentProps, type ComponentType, type ForwardedRef, forwardRef } from "react"; +import { Platform, type PressableProps } from "react-native"; +import type { SvgProps } from "react-native-svg"; +import type { YoshikiStyle } from "yoshiki"; +import { type Stylable, type Theme, px, useYoshiki } from "yoshiki/native"; import { PressableFeedback } from "./links"; -import { Breakpoint, focusReset, ts } from "./utils"; import { P } from "./text"; +import { type Breakpoint, focusReset, ts } from "./utils"; declare module "react" { function forwardRef( @@ -109,7 +110,7 @@ export const IconButton = forwardRef(function IconButton( +export const IconFab = ( props: ComponentProps>, ) => { const { css, theme } = useYoshiki(); diff --git a/front/packages/primitives/src/image/base-image.tsx b/front/packages/primitives/src/image/base-image.tsx index d44dbc68..33f4b251 100644 --- a/front/packages/primitives/src/image/base-image.tsx +++ b/front/packages/primitives/src/image/base-image.tsx @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ -import { KyooImage } from "@kyoo/models"; -import { ReactElement } from "react"; -import { ImageStyle } from "react-native"; -import { YoshikiStyle } from "yoshiki/src/type"; +import type { KyooImage } from "@kyoo/models"; +import type { ReactElement } from "react"; +import type { ImageStyle } from "react-native"; +import type { YoshikiStyle } from "yoshiki/src/type"; export type YoshikiEnhanced