Migrate from perttier/eslint to biome (#484)

This commit is contained in:
Zoe Roux 2024-05-10 17:52:33 +02:00 committed by GitHub
commit 30e2a5c867
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
139 changed files with 986 additions and 2342 deletions

View File

@ -25,18 +25,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node - name: Setup Biome
uses: actions/setup-node@v4 uses: biomejs/setup-biome@v2
with: with:
node-version: 18.x version: latest
cache: yarn
cache-dependency-path: front/yarn.lock
- name: Install dependencies - name: Run Biome
run: yarn install --immutable run: biome ci .
- name: Lint
run: yarn lint && yarn format
scanner: scanner:
name: "Lint scanner/autosync" name: "Lint scanner/autosync"

View File

@ -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 <https://www.gnu.org/licenses/>.",
" "
],
2
]
}
}

View File

@ -1 +0,0 @@
.yarn

View File

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

View File

@ -19,10 +19,10 @@
*/ */
import { Icon } from "@kyoo/primitives"; 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 Browse from "@material-symbols/svg-400/rounded/browse-fill.svg";
import Downloading from "@material-symbols/svg-400/rounded/downloading-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() { export default function TabsLayout() {
return ( return (

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Account, ConnectionErrorContext, useAccount } from "@kyoo/models"; import { type Account, ConnectionErrorContext, useAccount } from "@kyoo/models";
import { NavbarProfile, NavbarTitle } from "@kyoo/ui"; import { NavbarProfile, NavbarTitle } from "@kyoo/ui";
import { Redirect, Stack } from "expo-router"; import { Redirect, Stack } from "expo-router";
import { useContext, useRef } from "react"; import { useContext, useRef } from "react";
@ -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 (

View File

@ -20,39 +20,39 @@
import "react-native-reanimated"; 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 { import {
useFonts,
Poppins_300Light, Poppins_300Light,
Poppins_400Regular, Poppins_400Regular,
Poppins_900Black, Poppins_900Black,
useFonts,
} from "@expo-google-fonts/poppins"; } 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/en";
import "@formatjs/intl-displaynames/locale-data/fr"; 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. // TODO: use a backend to load jsons.
import en from "../../../translations/en.json"; import en from "../../../translations/en.json";
import fr from "../../../translations/fr.json"; import fr from "../../../translations/fr.json";
import zh from "../../../translations/zh.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) => { onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => { return NetInfo.addEventListener((state) => {

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Stack, useLocalSearchParams } from "expo-router"; import { type QueryPage, useHasPermission } from "@kyoo/models";
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 { Unauthorized } from "@kyoo/ui"; 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 = () => { const FullscreenProvider = () => {
useEffect(() => { useEffect(() => {

View File

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

View File

@ -1,6 +1,6 @@
{ {
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true
}, }
} }

View File

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

View File

@ -2,9 +2,7 @@
"name": "web", "name": "web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"sideEffects": [ "sideEffects": ["./src/polyfill.ts"],
"./src/polyfill.ts"
],
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@ -56,8 +54,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"

View File

@ -19,7 +19,7 @@
*/ */
import "i18next"; import "i18next";
import en from "../../../translations/en.json"; import type en from "../../../translations/en.json";
declare module "i18next" { declare module "i18next" {
interface CustomTypeOptions { interface CustomTypeOptions {

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentType, useMemo } from "react"; import i18next, { type InitOptions } from "i18next";
import i18next, { InitOptions } from "i18next"; import type { AppContext, AppInitialProps, AppProps } from "next/app";
import { type ComponentType, useMemo } from "react";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { AppContext, AppInitialProps, type AppProps } from "next/app";
import en from "../../../translations/en.json"; import en from "../../../translations/en.json";
import fr from "../../../translations/fr.json"; import fr from "../../../translations/fr.json";
@ -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}>

View File

@ -20,42 +20,42 @@
import "../polyfill"; import "../polyfill";
import { HydrationBoundary, QueryClientProvider, dehydrate } from "@tanstack/react-query"; import { PortalProvider } from "@gorhom/portal";
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 { import {
AccountP, AccountP,
AccountProvider, AccountProvider,
ConnectionErrorContext, ConnectionErrorContext,
type QueryIdentifier,
type QueryPage,
ServerInfoP,
UserP,
createQueryClient, createQueryClient,
fetchQuery, fetchQuery,
getTokenWJ, getTokenWJ,
QueryIdentifier,
QueryPage,
ServerInfoP,
setSsrApiUrl, setSsrApiUrl,
UserP,
useUserTheme, useUserTheme,
} from "@kyoo/models"; } 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 { 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 { 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" }); const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
@ -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>;
}; };

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { type DocumentContext, Head, Html, Main, NextScript } from "next/document";
import { AppRegistry } from "react-native"; import { AppRegistry } from "react-native";
import { Html, Main, Head, NextScript, DocumentContext } from "next/document"; import { StyleRegistryProvider, createStyleRegistry } from "yoshiki/web";
import { createStyleRegistry, StyleRegistryProvider } from "yoshiki/web";
export const style = ` export const style = `
/** /**

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { QueryPage, useHasPermission } from "@kyoo/models"; import { type QueryPage, useHasPermission } from "@kyoo/models";
import { Unauthorized } from "@kyoo/ui"; import { Unauthorized } from "@kyoo/ui";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ComponentType } from "react"; import type { ComponentType } from "react";
export const withRoute = <Props,>( export const withRoute = <Props,>(
Component: ComponentType<Props>, Component: ComponentType<Props>,

View File

@ -16,9 +16,9 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["src/*"], "~/*": ["src/*"]
}, }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
} }

51
front/biome.json Normal file
View File

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

View File

@ -10,28 +10,13 @@
"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": [
"apps/*",
"packages/*"
],
"prettier": {
"useTabs": true,
"printWidth": 100,
"trailingComma": "all"
}, },
"workspaces": ["apps/*", "packages/*"],
"devDependencies": { "devDependencies": {
"eslint": "8.56.0", "@biomejs/biome": "1.7.3",
"eslint-config-next": "14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-header": "^3.1.1",
"prettier": "^3.2.4",
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"packageManager": "yarn@3.2.4" "packageManager": "yarn@3.2.4"

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ZodTypeAny, z } from "zod";
import { Account, AccountP } from "./accounts";
import { MMKV } from "react-native-mmkv";
import { Platform } from "react-native"; 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(); export const storage = new MMKV();
@ -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;

View File

@ -18,17 +18,25 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
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 { useQueryClient } from "@tanstack/react-query";
import { atom, getDefaultStore, useAtomValue, useSetAtom } from "jotai"; 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 { useFetch } from "./query";
import { KyooErrors } from "./kyoo-errors"; import { ServerInfoP, type User, UserP } from "./resources";
import { zdate } from "./utils";
export const TokenP = z.object({ export const TokenP = z.object({
token_type: z.literal("Bearer"), token_type: z.literal("Bearer"),
@ -72,7 +80,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 +122,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 +158,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)
) { ) {

View File

@ -18,13 +18,13 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
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 { 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<A, B> = type Result<A, B> =
| { ok: true; value: A; error?: undefined } | { ok: true; value: A; error?: undefined }
@ -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();

View File

@ -18,18 +18,18 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentType, ReactElement } from "react";
import { import {
QueryClient, QueryClient,
QueryFunctionContext, type QueryFunctionContext,
useInfiniteQuery, useInfiniteQuery,
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { z } from "zod"; import type { ComponentType, ReactElement } from "react";
import { KyooErrors } from "./kyoo-errors"; import type { z } from "zod";
import { Page, Paged } from "./page";
import { getToken, getTokenWJ } from "./login";
import { getCurrentApiUrl } from "."; 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!; export let lastUsedUrl: string = null!;
@ -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;

View File

@ -19,9 +19,9 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, imageFn } from "../traits"; import { ImagesP, imageFn } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
import { zdate } from "../utils";
export const BaseEpisodeP = ResourceP("episode") export const BaseEpisodeP = ResourceP("episode")
.merge(ImagesP) .merge(ImagesP)

View File

@ -19,8 +19,8 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { ShowP } from "./show";
import { BaseEpisodeP } from "./episode.base"; import { BaseEpisodeP } from "./episode.base";
import { ShowP } from "./show";
import { WatchStatusP } from "./watch-status"; import { WatchStatusP } from "./watch-status";
export const EpisodeP = BaseEpisodeP.and( export const EpisodeP = BaseEpisodeP.and(

View File

@ -19,13 +19,13 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, ResourceP, imageFn } from "../traits"; import { ImagesP, ResourceP, imageFn } from "../traits";
import { Genre } from "./genre"; import { zdate } from "../utils";
import { StudioP } from "./studio";
import { Status } from "./show";
import { CollectionP } from "./collection"; import { CollectionP } from "./collection";
import { Genre } from "./genre";
import { MetadataP } from "./metadata"; import { MetadataP } from "./metadata";
import { Status } from "./show";
import { StudioP } from "./studio";
import { WatchStatusP } from "./watch-status"; import { WatchStatusP } from "./watch-status";
export const MovieP = ResourceP("movie") export const MovieP = ResourceP("movie")

View File

@ -19,8 +19,8 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { MovieP } from "./movie";
import { EpisodeP } from "./episode"; import { EpisodeP } from "./episode";
import { MovieP } from "./movie";
export const NewsP = z.union([ export const NewsP = z.union([
/* /*

View File

@ -19,8 +19,8 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, ResourceP } from "../traits"; import { ImagesP, ResourceP } from "../traits";
import { zdate } from "../utils";
export const SeasonP = ResourceP("season").merge(ImagesP).extend({ export const SeasonP = ResourceP("season").merge(ImagesP).extend({
/** /**

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Platform } from "react-native";
import { z } from "zod"; import { z } from "zod";
import { imageFn } from ".."; import { imageFn } from "..";
import { Platform } from "react-native";
export const OidcInfoP = z.object({ export const OidcInfoP = z.object({
/* /*

View File

@ -19,12 +19,12 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { zdate } from "../utils";
import { ImagesP, ResourceP } from "../traits"; import { ImagesP, ResourceP } from "../traits";
import { Genre } from "./genre"; import { zdate } from "../utils";
import { StudioP } from "./studio";
import { BaseEpisodeP } from "./episode.base"; import { BaseEpisodeP } from "./episode.base";
import { Genre } from "./genre";
import { MetadataP } from "./metadata"; import { MetadataP } from "./metadata";
import { StudioP } from "./studio";
import { ShowWatchStatusP } from "./watch-status"; import { ShowWatchStatusP } from "./watch-status";
/** /**

View File

@ -19,8 +19,8 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { ResourceP } from "../traits/resource";
import { imageFn } from "../traits/images"; import { imageFn } from "../traits/images";
import { ResourceP } from "../traits/resource";
export const UserP = ResourceP("user") export const UserP = ResourceP("user")
.extend({ .extend({

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import i18next from "i18next";
import { z } from "zod"; import { z } from "zod";
import { imageFn } from "../traits"; import { imageFn } from "../traits";
import i18next from "i18next";
import { QualityP } from "./quality"; import { QualityP } from "./quality";
const getDisplayName = (sub: Track) => { 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 // 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];
}; };

View File

@ -18,13 +18,12 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Platform } from "react-native";
import { useMMKVString } from "react-native-mmkv"; import { useMMKVString } from "react-native-mmkv";
import { setCookie, storage } from "./account-internal"; import { setCookie, storage } from "./account-internal";
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";

View File

@ -19,10 +19,10 @@
*/ */
import { Platform } from "react-native"; import { Platform } from "react-native";
import { Movie, Show } from "./resources";
import { z } from "zod";
import { useMMKVString } from "react-native-mmkv"; import { useMMKVString } from "react-native-mmkv";
import { z } from "zod";
import { storage } from "./account-internal"; import { storage } from "./account-internal";
import type { Movie, Show } from "./resources";
export const zdate = z.coerce.date; export const zdate = z.coerce.date;
@ -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();
} }
}; };

View File

@ -18,9 +18,9 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["src/*"], "~/*": ["src/*"]
}, }
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
} }

View File

@ -20,8 +20,8 @@
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 // 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 AlertButton, type AlertOptions, Alert as RNAlert } from "react-native";
import { type SweetAlertIcon } from "sweetalert2"; import type { SweetAlertIcon } from "sweetalert2";
export interface ExtendedAlertStatic { export interface ExtendedAlertStatic {
alert: ( alert: (

View File

@ -20,9 +20,10 @@
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 // 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"; 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,

View File

@ -18,23 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { View, ViewStyle, Image, ImageProps } from "react-native"; import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
import { useYoshiki, px, Stylable } from "yoshiki/native"; 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 { Icon } from "./icons";
import { P } from "./text"; 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) => { 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);
} }

View File

@ -18,12 +18,12 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentType, ForwardedRef, ReactElement, forwardRef } from "react"; import { type ComponentType, type ForwardedRef, type ReactElement, forwardRef } from "react";
import { Theme, useYoshiki } from "yoshiki/native"; import { type Falsy, type PressableProps, View } from "react-native";
import { type Theme, useYoshiki } from "yoshiki/native";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { P } from "./text"; import { P } from "./text";
import { ts } from "./utils"; import { ts } from "./utils";
import { Falsy, PressableProps, View } from "react-native";
export const Button = forwardRef(function Button<AsProps = PressableProps>( export const Button = forwardRef(function Button<AsProps = PressableProps>(
{ {

View File

@ -18,12 +18,12 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
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 { Link } from "./links";
import { Skeleton } from "./skeleton";
import { P } from "./text"; import { P } from "./text";
import { capitalize, ts } from "./utils"; import { capitalize, ts } from "./utils";
import { TextProps } from "react-native";
import { Skeleton } from "./skeleton";
export const Chip = ({ export const Chip = ({
color, color,
@ -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

View File

@ -18,11 +18,11 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentType } from "react"; import type { ComponentType } from "react";
import { View, ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { percent, px, useYoshiki } from "yoshiki/native"; import { percent, px, useYoshiki } from "yoshiki/native";
export const Container = <AsProps = ViewProps,>({ export const Container = <AsProps = ViewProps>({
as, as,
...props ...props
}: { as?: ComponentType<AsProps> } & AsProps) => { }: { as?: ComponentType<AsProps> } & AsProps) => {

View File

@ -19,7 +19,7 @@
*/ */
import { HR as EHR } from "@expo/html-elements"; 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"; import { ts } from "./utils";
export const HR = ({ export const HR = ({

View File

@ -18,14 +18,15 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React, { ComponentProps, ComponentType, ForwardedRef, forwardRef } from "react"; import type React from "react";
import { Platform, PressableProps } from "react-native"; import { type ComponentProps, type ComponentType, type ForwardedRef, forwardRef } from "react";
import { SvgProps } from "react-native-svg"; import { Platform, type PressableProps } from "react-native";
import { YoshikiStyle } from "yoshiki"; import type { SvgProps } from "react-native-svg";
import { px, Stylable, Theme, useYoshiki } from "yoshiki/native"; import type { YoshikiStyle } from "yoshiki";
import { type Stylable, type Theme, px, useYoshiki } from "yoshiki/native";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { Breakpoint, focusReset, ts } from "./utils";
import { P } from "./text"; import { P } from "./text";
import { type Breakpoint, focusReset, ts } from "./utils";
declare module "react" { declare module "react" {
function forwardRef<T, P = {}>( function forwardRef<T, P = {}>(
@ -109,7 +110,7 @@ export const IconButton = forwardRef(function IconButton<AsProps = PressableProp
); );
}); });
export const IconFab = <AsProps = PressableProps,>( export const IconFab = <AsProps = PressableProps>(
props: ComponentProps<typeof IconButton<AsProps>>, props: ComponentProps<typeof IconButton<AsProps>>,
) => { ) => {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { KyooImage } from "@kyoo/models"; import type { KyooImage } from "@kyoo/models";
import { ReactElement } from "react"; import type { ReactElement } from "react";
import { ImageStyle } from "react-native"; import type { ImageStyle } from "react-native";
import { YoshikiStyle } from "yoshiki/src/type"; import type { YoshikiStyle } from "yoshiki/src/type";
export type YoshikiEnhanced<Style> = Style extends any export type YoshikiEnhanced<Style> = Style extends any
? { ? {
@ -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;
}; };

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ReactElement } from "react"; import type { ReactElement } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { Blurhash } from "react-native-blurhash"; import { Blurhash } from "react-native-blurhash";
import { Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, useYoshiki } from "yoshiki/native";
export const BlurhashContainer = ({ export const BlurhashContainer = ({
blurhash, blurhash,

View File

@ -20,19 +20,16 @@
import { decode } from "blurhash"; import { decode } from "blurhash";
import { import {
HTMLAttributes, type HTMLAttributes,
ReactElement, type 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;

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useState } from "react";
import { FlexStyle, ImageStyle, View, ViewStyle } from "react-native";
import FastImage from "react-native-fast-image";
import { Blurhash } from "react-native-blurhash";
import { percent, useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { Skeleton } from "../skeleton";
import { getCurrentToken } from "@kyoo/models"; import { getCurrentToken } from "@kyoo/models";
import { useState } from "react";
import { type FlexStyle, type ImageStyle, View, type ViewStyle } from "react-native";
import { Blurhash } from "react-native-blurhash";
import FastImage from "react-native-fast-image";
import { percent, useYoshiki } from "yoshiki/native";
import { Skeleton } from "../skeleton";
import type { ImageLayout, Props } from "./base-image";
export const Image = ({ export const Image = ({
src, src,
@ -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)} />
); );

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useState } from "react";
import { ImageStyle, View, ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { BlurhashContainer } from "./blurhash.web";
import { Skeleton } from "../skeleton";
import NextImage from "next/image"; import NextImage from "next/image";
import { useState } from "react";
import { type ImageStyle, View, type ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { imageBorderRadius } from "../constants"; import { imageBorderRadius } from "../constants";
import { Skeleton } from "../skeleton";
import type { ImageLayout, Props } from "./base-image";
import { BlurhashContainer } from "./blurhash.web";
export const Image = ({ export const Image = ({
src, src,
@ -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)} />
); );

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ImageStyle, View, ViewProps, ViewStyle } from "react-native"; import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient";
import { Props, ImageLayout, YoshikiEnhanced } from "./base-image"; import type { ComponentProps, ComponentType, ReactNode } from "react";
import { Image } from "./image"; import { type ImageStyle, View, type ViewProps, type ViewStyle } from "react-native";
import { ComponentProps, ComponentType, ReactNode } from "react";
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
import { ContrastArea } from "../themes";
import { percent } from "yoshiki/native"; import { percent } from "yoshiki/native";
import { imageBorderRadius } from "../constants"; import { imageBorderRadius } from "../constants";
import { ContrastArea } from "../themes";
import type { ImageLayout, Props, YoshikiEnhanced } from "./base-image";
import { Image } from "./image";
export { Sprite } from "./sprite"; export { Sprite } from "./sprite";
export { BlurhashContainer } from "./blurhash"; export { BlurhashContainer } from "./blurhash";
@ -55,7 +55,7 @@ export const PosterBackground = ({
/> />
); );
export const ImageBackground = <AsProps = ViewProps,>({ export const ImageBackground = <AsProps = ViewProps>({
src, src,
alt, alt,
quality, quality,
@ -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;
})} })}

View File

@ -18,11 +18,11 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { forwardRef, ReactNode, useState } from "react"; import { type ReactNode, forwardRef, useState } from "react";
import { TextInput, TextInputProps, View, ViewStyle } from "react-native"; import { TextInput, type TextInputProps, View, type ViewStyle } from "react-native";
import { px, Theme, useYoshiki } from "yoshiki/native"; import { type Theme, px, useYoshiki } from "yoshiki/native";
import type { YoshikiEnhanced } from "./image/base-image";
import { focusReset, ts } from "./utils"; import { focusReset, ts } from "./utils";
import { YoshikiEnhanced } from "./image/base-image";
export const Input = forwardRef< export const Input = forwardRef<
TextInput, TextInput,

View File

@ -18,13 +18,20 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { forwardRef, ReactNode } from "react"; import type { UrlObject } from "node:url";
import { Platform, Pressable, TextProps, View, PressableProps, Linking } from "react-native"; import { type ReactNode, forwardRef } from "react";
import {
Linking,
Platform,
Pressable,
type PressableProps,
type TextProps,
type View,
} from "react-native";
import { TextLink, useLink } from "solito/link"; import { TextLink, useLink } from "solito/link";
import { useTheme, useYoshiki } from "yoshiki/native";
import type { UrlObject } from "url";
import { alpha } from "./themes";
import { parseNextPath } from "solito/router"; import { parseNextPath } from "solito/router";
import { useTheme, useYoshiki } from "yoshiki/native";
import { alpha } from "./themes";
export const A = ({ export const A = ({
href, href,

View File

@ -19,29 +19,29 @@
*/ */
import { Portal } from "@gorhom/portal"; import { Portal } from "@gorhom/portal";
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
import { ScrollView } from "moti"; import { ScrollView } from "moti";
import { import {
ComponentType, type ComponentType,
type ReactElement,
type ReactNode,
createContext, createContext,
ReactElement,
ReactNode,
useContext, useContext,
useEffect, useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { StyleSheet, Pressable, View } from "react-native"; import { Pressable, StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { SvgProps } from "react-native-svg";
import { useRouter } from "solito/router";
import { percent, px, sm, useYoshiki, vh, xl } from "yoshiki/native"; import { percent, px, sm, useYoshiki, vh, xl } from "yoshiki/native";
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
import { Icon, IconButton } from "./icons"; import { Icon, IconButton } from "./icons";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { P } from "./text"; import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./themes"; import { ContrastArea, SwitchVariant } from "./themes";
import { ts } from "./utils"; import { ts } from "./utils";
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
import { useRouter } from "solito/router";
import { SvgProps } from "react-native-svg";
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined); const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
@ -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.

View File

@ -18,18 +18,24 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Dot from "@material-symbols/svg-400/rounded/fiber_manual_record-fill.svg";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { ComponentProps, ComponentType, forwardRef, ReactElement, ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { PressableProps } from "react-native"; import {
import { useYoshiki } from "yoshiki/web"; type ComponentProps,
type ComponentType,
type ReactElement,
type ReactNode,
forwardRef,
} from "react";
import type { PressableProps } from "react-native";
import type { SvgProps } from "react-native-svg";
import { useYoshiki as useNativeYoshiki } from "yoshiki/native"; import { useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { useYoshiki } from "yoshiki/web";
import { Icon } from "./icons";
import { P } from "./text"; import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./themes"; import { ContrastArea, SwitchVariant } from "./themes";
import { Icon } from "./icons";
import Dot from "@material-symbols/svg-400/rounded/fiber_manual_record-fill.svg";
import { focusReset, ts } from "./utils"; import { focusReset, ts } from "./utils";
import { SvgProps } from "react-native-svg";
type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T; type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
export const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => { export const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => {

View File

@ -18,13 +18,13 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ReactNode, useCallback, useEffect, useState } from "react";
import { Container } from "./container";
import { usePortal } from "@gorhom/portal"; import { usePortal } from "@gorhom/portal";
import { ContrastArea, SwitchVariant, YoshikiFunc } from "./themes"; import { type ReactNode, useCallback, useEffect, useState } from "react";
import { View, ScrollView } from "react-native"; import { ScrollView, View } from "react-native";
import { imageBorderRadius } from "./constants";
import { px, vh } from "yoshiki/native"; import { px, vh } from "yoshiki/native";
import { imageBorderRadius } from "./constants";
import { Container } from "./container";
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes";
import { ts } from "./utils"; import { ts } from "./utils";
export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => { export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {

View File

@ -20,7 +20,7 @@
import { ActivityIndicator, Platform, View } from "react-native"; import { ActivityIndicator, Platform, View } from "react-native";
import { Circle, Svg } from "react-native-svg"; import { Circle, Svg } from "react-native-svg";
import { px, Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, px, useYoshiki } from "yoshiki/native";
export const CircularProgress = ({ export const CircularProgress = ({
size = 48, size = 48,

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Icon } from "./icons";
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg"; import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
import { Menu } from "./menu";
import { Button } from "./button"; import { Button } from "./button";
import { Icon } from "./icons";
import { Menu } from "./menu";
export const Select = <Value extends string>({ export const Select = <Value extends string>({
label, label,

View File

@ -18,20 +18,20 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
import * as RSelect from "@radix-ui/react-select"; import * as RSelect from "@radix-ui/react-select";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Icon } from "./icons";
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
import { ContrastArea, SwitchVariant } from "./themes";
import { InternalTriger, YoshikiProvider } from "./menu.web";
import { Theme, px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { useYoshiki } from "yoshiki";
import { PressableFeedback } from "./links";
import { P } from "./text";
import { focusReset, ts } from "./utils";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki";
import { type Theme, px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { Icon } from "./icons";
import { PressableFeedback } from "./links";
import { InternalTriger, YoshikiProvider } from "./menu.web";
import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./themes";
import { focusReset, ts } from "./utils";
export const Select = ({ export const Select = ({
label, label,

View File

@ -19,10 +19,10 @@
*/ */
import { LinearGradient as LG } from "expo-linear-gradient"; import { LinearGradient as LG } from "expo-linear-gradient";
import { AnimatePresence, motify, MotiView } from "moti"; import { AnimatePresence, MotiView, motify } from "moti";
import { useState } from "react"; import { useState } from "react";
import { Platform, View, ViewProps } from "react-native"; import { Platform, View, type ViewProps } from "react-native";
import { px, rem, useYoshiki, percent, em } from "yoshiki/native"; import { em, percent, px, rem, useYoshiki } from "yoshiki/native";
import { hiddenIfNoJs } from "./utils/nojs"; import { hiddenIfNoJs } from "./utils/nojs";
const LinearGradient = motify(LG)(); const LinearGradient = motify(LG)();

View File

@ -19,8 +19,8 @@
*/ */
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { View, ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { px, rem, useYoshiki, percent, em } from "yoshiki/native"; import { em, percent, px, rem, useYoshiki } from "yoshiki/native";
import { hiddenIfNoJs } from "./utils/nojs"; import { hiddenIfNoJs } from "./utils/nojs";
export const SkeletonCss = () => ( export const SkeletonCss = () => (

View File

@ -19,10 +19,10 @@
*/ */
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { GestureResponderEvent, Platform, View } from "react-native"; import { type GestureResponderEvent, Platform, View } from "react-native";
import { px, percent, Stylable, useYoshiki } from "yoshiki/native"; import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
import { Stylable, percent, px, useYoshiki } from "yoshiki/native";
import { focusReset } from "./utils"; import { focusReset } from "./utils";
import { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
export const Slider = ({ export const Slider = ({
progress, progress,

View File

@ -19,14 +19,14 @@
*/ */
import { usePortal } from "@gorhom/portal"; import { usePortal } from "@gorhom/portal";
import { ReactElement, createContext, useCallback, useContext, useRef } from "react"; import { type ReactElement, createContext, useCallback, useContext, useRef } from "react";
import { ContrastArea, SwitchVariant } from "./themes";
import { P } from "./text";
import { View } from "react-native"; import { View } from "react-native";
import { percent, px } from "yoshiki/native"; import { percent, px } from "yoshiki/native";
import { ts } from "./utils";
import { Button } from "./button"; import { Button } from "./button";
import { imageBorderRadius } from "./constants"; import { imageBorderRadius } from "./constants";
import { P } from "./text";
import { ContrastArea, SwitchVariant } from "./themes";
import { ts } from "./utils";
export type Snackbar = { export type Snackbar = {
key?: string; key?: string;

View File

@ -19,8 +19,8 @@
*/ */
declare module "*.svg" { declare module "*.svg" {
import React from "react"; import type React from "react";
import { SvgProps } from "react-native-svg"; import type { SvgProps } from "react-native-svg";
const content: React.FC<SvgProps>; const content: React.FC<SvgProps>;
export default content; export default content;
} }

View File

@ -18,9 +18,6 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentType, ComponentProps } from "react";
import { Platform, Text, TextProps, TextStyle, StyleProp } from "react-native";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { import {
H1 as EH1, H1 as EH1,
H2 as EH2, H2 as EH2,
@ -30,6 +27,9 @@ import {
H6 as EH6, H6 as EH6,
P as EP, P as EP,
} from "@expo/html-elements"; } from "@expo/html-elements";
import type { ComponentProps, ComponentType } from "react";
import { Platform, type StyleProp, Text, type TextProps, type TextStyle } from "react-native";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { ts } from "./utils/spacing"; import { ts } from "./utils/spacing";
const styleText = ( const styleText = (

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ThemeBuilder } from "./theme"; import type { ThemeBuilder } from "./theme";
// Ref: https://github.com/catppuccin/catppuccin // Ref: https://github.com/catppuccin/catppuccin
export const catppuccin: ThemeBuilder = { export const catppuccin: ThemeBuilder = {

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ReactNode } from "react"; import type { Property } from "csstype";
import { Property } from "csstype"; import type { ReactNode } from "react";
import { Theme, ThemeProvider, useAutomaticTheme } from "yoshiki"; import { Platform } from "react-native";
import { useTheme, useYoshiki } from "yoshiki/native"; import { type Theme, ThemeProvider, useAutomaticTheme } from "yoshiki";
import "yoshiki"; import "yoshiki";
import { useTheme, useYoshiki } from "yoshiki/native";
import "yoshiki/native"; import "yoshiki/native";
import { catppuccin } from "./catppuccin"; import { catppuccin } from "./catppuccin";
import { Platform } from "react-native";
type FontList = Partial< type FontList = Partial<
Record< Record<
@ -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,

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Theme } from "yoshiki/native";
import { Tooltip as RTooltip } from "react-tooltip";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Tooltip as RTooltip } from "react-tooltip";
import type { Theme } from "yoshiki/native";
import { ContrastArea } from "./themes"; import { ContrastArea } from "./themes";
export const tooltip = (tooltip: string, up?: boolean) => ({ export const tooltip = (tooltip: string, up?: boolean) => ({

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React from "react"; import type React from "react";
import "react-native"; import "react-native";
declare module "react-native" { declare module "react-native" {

View File

@ -19,7 +19,7 @@
*/ */
import { useWindowDimensions } from "react-native"; import { useWindowDimensions } from "react-native";
import { Breakpoints as YoshikiBreakpoint, isBreakpoints, breakpoints } from "yoshiki/native"; import { type Breakpoints as YoshikiBreakpoint, breakpoints, isBreakpoints } from "yoshiki/native";
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]; type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>; export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;

View File

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

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ViewProps } from "react-native"; import type { ViewProps } from "react-native";
export const hiddenIfNoJs: ViewProps = { style: { $$css: true, noJs: "noJsHidden" } as any }; export const hiddenIfNoJs: ViewProps = { style: { $$css: true, noJs: "noJsHidden" } as any };

View File

@ -19,7 +19,7 @@
*/ */
import { useEffect } from "react"; import { useEffect } from "react";
import { Platform, ViewProps } from "react-native"; import { Platform, type ViewProps } from "react-native";
export const TouchOnlyCss = () => { export const TouchOnlyCss = () => {
return ( return (

View File

@ -18,9 +18,9 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["src/*"], "~/*": ["src/*"]
}, }
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
} }

View File

@ -18,12 +18,12 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { QueryPage } from "@kyoo/models"; import type { QueryPage } from "@kyoo/models";
import { ts } from "@kyoo/primitives"; import { ts } from "@kyoo/primitives";
import { ScrollView } from "react-native"; import { ScrollView } from "react-native";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { UserList } from "./users";
import { Scanner } from "./scanner"; import { Scanner } from "./scanner";
import { UserList } from "./users";
export const AdminPage: QueryPage = () => { export const AdminPage: QueryPage = () => {
return ( return (

View File

@ -18,14 +18,14 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Issue, IssueP, QueryIdentifier, queryFn, useFetch } from "@kyoo/models"; import { type Issue, IssueP, type QueryIdentifier, queryFn, useFetch } from "@kyoo/models";
import { useTranslation } from "react-i18next";
import { SettingsContainer } from "../settings/base";
import { Button, Icon, P, Skeleton, tooltip, ts } from "@kyoo/primitives"; import { Button, Icon, P, Skeleton, tooltip, ts } from "@kyoo/primitives";
import { ErrorView } from "../errors"; import { useTranslation } from "react-i18next";
import { z } from "zod";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { z } from "zod";
import { ErrorView } from "../errors";
import { SettingsContainer } from "../settings/base";
import Info from "@material-symbols/svg-400/outlined/info.svg"; import Info from "@material-symbols/svg-400/outlined/info.svg";
import Scan from "@material-symbols/svg-400/outlined/sensors.svg"; import Scan from "@material-symbols/svg-400/outlined/sensors.svg";
@ -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>
); );

View File

@ -18,21 +18,21 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { QueryIdentifier, User, UserP, queryFn } from "@kyoo/models"; import { type QueryIdentifier, type User, UserP, queryFn } from "@kyoo/models";
import { Alert, Avatar, Icon, IconButton, Menu, P, Skeleton, tooltip, ts } from "@kyoo/primitives"; import { Alert, Avatar, Icon, IconButton, Menu, P, Skeleton, tooltip, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { px, useYoshiki } from "yoshiki/native"; import { px, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch"; import type { Layout, WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { SettingsContainer } from "../settings/base"; import { SettingsContainer } from "../settings/base";
import UserI from "@material-symbols/svg-400/rounded/account_circle.svg"; import UserI from "@material-symbols/svg-400/rounded/account_circle.svg";
import Delete from "@material-symbols/svg-400/rounded/delete.svg"; import Delete from "@material-symbols/svg-400/rounded/delete.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import Verifed from "@material-symbols/svg-400/rounded/verified_user.svg";
import Unverifed from "@material-symbols/svg-400/rounded/gpp_bad.svg"; import Unverifed from "@material-symbols/svg-400/rounded/gpp_bad.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import Admin from "@material-symbols/svg-400/rounded/shield_person.svg"; import Admin from "@material-symbols/svg-400/rounded/shield_person.svg";
import Verifed from "@material-symbols/svg-400/rounded/verified_user.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
export const UserGrid = ({ export const UserGrid = ({

View File

@ -18,24 +18,24 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { KyooImage, WatchStatusV } from "@kyoo/models"; import { type KyooImage, WatchStatusV } from "@kyoo/models";
import { import {
Link,
Skeleton,
ts,
focusReset,
P,
SubP,
PosterBackground,
Icon, Icon,
Link,
P,
PosterBackground,
Skeleton,
SubP,
focusReset,
important, important,
ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { ImageStyle, Platform, View } from "react-native";
import { max, percent, px, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
import Done from "@material-symbols/svg-400/rounded/done-fill.svg"; import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
import { ItemContext } from "../components/context-menus";
import { useState } from "react"; import { useState } from "react";
import { type ImageStyle, Platform, View } from "react-native";
import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native";
import { ItemContext } from "../components/context-menus";
import type { Layout, WithLoading } from "../fetch";
export const ItemWatchStatus = ({ export const ItemWatchStatus = ({
watchStatus, watchStatus,

View File

@ -29,17 +29,17 @@ import {
tooltip, tooltip,
ts, ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { useYoshiki } from "yoshiki/native";
import Style from "@material-symbols/svg-400/rounded/style.svg";
import GridView from "@material-symbols/svg-400/rounded/grid_view.svg";
import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
import Sort from "@material-symbols/svg-400/rounded/sort.svg";
import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg"; import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg";
import { Layout, SearchSort, SortOrd } from "./types"; import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
import GridView from "@material-symbols/svg-400/rounded/grid_view.svg";
import Sort from "@material-symbols/svg-400/rounded/sort.svg";
import Style from "@material-symbols/svg-400/rounded/style.svg";
import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { View, PressableProps } from "react-native"; import { useTranslation } from "react-i18next";
import { type PressableProps, View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { Layout, SearchSort, SortOrd } from "./types";
const SortTrigger = forwardRef<View, PressableProps & { sortKey: string }>(function SortTrigger( const SortTrigger = forwardRef<View, PressableProps & { sortKey: string }>(function SortTrigger(
{ sortKey, ...props }, { sortKey, ...props },
@ -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" })}
/> />

View File

@ -19,21 +19,21 @@
*/ */
import { import {
QueryIdentifier, type LibraryItem,
QueryPage,
LibraryItem,
LibraryItemP, LibraryItemP,
type QueryIdentifier,
type QueryPage,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { ComponentProps, useState } from "react"; import { type ComponentProps, useState } from "react";
import { createParam } from "solito"; import { createParam } from "solito";
import { DefaultLayout } from "../layout"; import type { WithLoading } from "../fetch";
import { WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { DefaultLayout } from "../layout";
import { ItemGrid } from "./grid"; import { ItemGrid } from "./grid";
import { ItemList } from "./list";
import { SortBy, SortOrd, Layout } from "./types";
import { BrowseSettings } from "./header"; import { BrowseSettings } from "./header";
import { ItemList } from "./list";
import { Layout, SortBy, SortOrd } from "./types";
const { useParam } = createParam<{ sortBy?: string }>(); const { useParam } = createParam<{ sortBy?: string }>();

View File

@ -18,24 +18,24 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { KyooImage, WatchStatusV } from "@kyoo/models"; import type { KyooImage, WatchStatusV } from "@kyoo/models";
import { import {
Heading,
ImageBackground,
Link, Link,
P, P,
Skeleton,
ts,
ImageBackground,
Heading,
PosterBackground, PosterBackground,
Skeleton,
imageBorderRadius, imageBorderRadius,
important, important,
ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { useState } from "react"; import { useState } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { percent, px, rem, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
import { ItemWatchStatus } from "./grid";
import { ItemContext } from "../components/context-menus"; import { ItemContext } from "../components/context-menus";
import type { Layout, WithLoading } from "../fetch";
import { ItemWatchStatus } from "./grid";
export const ItemList = ({ export const ItemList = ({
href, href,

View File

@ -19,26 +19,26 @@
*/ */
import { import {
Collection, type Collection,
CollectionP, CollectionP,
LibraryItem, type LibraryItem,
LibraryItemP, LibraryItemP,
QueryIdentifier, type QueryIdentifier,
QueryPage, type QueryPage,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { Header as ShowHeader, TitleLine } from "../details/header";
import { Container, Head, ImageBackground, P, Skeleton, ts, usePageStyle } from "@kyoo/primitives"; import { Container, Head, ImageBackground, P, Skeleton, ts, usePageStyle } from "@kyoo/primitives";
import { percent, px, useYoshiki } from "yoshiki/native";
import { useTranslation } from "react-i18next";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Platform, View, ViewProps } from "react-native"; import { useTranslation } from "react-i18next";
import { Platform, View, type ViewProps } from "react-native";
import { percent, px, useYoshiki } from "yoshiki/native";
import { ItemGrid } from "../browse/grid";
import { Header as ShowHeader, TitleLine } from "../details/header";
import { SvgWave } from "../details/show";
import { Fetch } from "../fetch"; import { Fetch } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { DefaultLayout } from "../layout";
import { ItemDetails } from "../home/recommended"; import { ItemDetails } from "../home/recommended";
import { SvgWave } from "../details/show"; import { DefaultLayout } from "../layout";
import { ItemGrid } from "../browse/grid";
const Header = ({ slug }: { slug: string }) => { const Header = ({ slug }: { slug: string }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();

View File

@ -26,7 +26,7 @@ import Info from "@material-symbols/svg-400/rounded/info.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";

View File

@ -18,13 +18,19 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Audio, QueryIdentifier, Subtitle, WatchInfo, WatchInfoP } from "@kyoo/models"; import {
type Audio,
type QueryIdentifier,
type Subtitle,
type WatchInfo,
WatchInfoP,
} from "@kyoo/models";
import { Button, HR, P, Popup, Skeleton } from "@kyoo/primitives"; import { Button, HR, P, Popup, Skeleton } from "@kyoo/primitives";
import { Fetch } from "../fetch"; import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { Fragment } from "react"; import { Fetch } from "../fetch";
const MediaInfoTable = ({ const MediaInfoTable = ({
mediaInfo: { path, video, container, audios, subtitles, duration, size }, mediaInfo: { path, video, container, audios, subtitles, duration, size },
@ -35,15 +41,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 +56,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 +100,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>
)), )),
)} )}

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Breakpoint, Icon, P, Skeleton, ts } from "@kyoo/primitives"; import { type Breakpoint, Icon, P, Skeleton, ts } from "@kyoo/primitives";
import { View } from "react-native";
import Star from "@material-symbols/svg-400/rounded/star-fill.svg"; import Star from "@material-symbols/svg-400/rounded/star-fill.svg";
import { View } from "react-native";
import { rem, useYoshiki } from "yoshiki/native"; import { rem, useYoshiki } from "yoshiki/native";
export const Rating = ({ rating, color }: { rating?: number; color: Breakpoint<string> }) => { export const Rating = ({ rating, color }: { rating?: number; color: Breakpoint<string> }) => {

View File

@ -18,15 +18,15 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { WatchStatusV, queryFn, useAccount } from "@kyoo/models";
import { IconButton, Menu, tooltip } from "@kyoo/primitives"; import { IconButton, Menu, tooltip } from "@kyoo/primitives";
import { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg"; import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.svg"; import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.svg";
import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg"; import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { WatchStatusV, queryFn, useAccount } from "@kyoo/models"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
export const watchListIcon = (status: WatchStatusV | null) => { export const watchListIcon = (status: WatchStatusV | null) => {
switch (status) { switch (status) {

View File

@ -19,15 +19,15 @@
*/ */
import { import {
Collection, type Collection,
CollectionP, CollectionP,
KyooImage, type KyooImage,
QueryIdentifier, type QueryIdentifier,
useInfiniteFetch, useInfiniteFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { Container, H2, ImageBackground, Link, P, focusReset, ts } from "@kyoo/primitives"; import { Container, H2, ImageBackground, Link, P, focusReset, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Theme, useYoshiki } from "yoshiki/native"; import { type Theme, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../errors"; import { ErrorView } from "../errors";
export const PartOf = ({ export const PartOf = ({

View File

@ -18,30 +18,30 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { type KyooImage, WatchStatusV } from "@kyoo/models";
import { import {
focusReset,
H6, H6,
IconButton, IconButton,
ImageBackground, ImageBackground,
ImageProps, type ImageProps,
important,
Link, Link,
P, P,
Skeleton, Skeleton,
SubP, SubP,
focusReset,
important,
tooltip, tooltip,
ts, ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ImageStyle, Platform, PressableProps, View } from "react-native"; import { type ImageStyle, Platform, type PressableProps, View } from "react-native";
import { Layout, WithLoading } from "../fetch"; import { type Stylable, type Theme, percent, rem, useYoshiki } from "yoshiki/native";
import { percent, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { KyooImage, WatchStatusV } from "@kyoo/models";
import { ItemProgress } from "../browse/grid"; import { ItemProgress } from "../browse/grid";
import { EpisodesContext } from "../components/context-menus"; import { EpisodesContext } from "../components/context-menus";
import { useState } from "react"; import type { Layout, WithLoading } from "../fetch";
import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg";
import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg";
export const episodeDisplayNumber = ( export const episodeDisplayNumber = (
episode: { episode: {
@ -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>

View File

@ -19,17 +19,17 @@
*/ */
import { import {
Genre, type Genre,
KyooImage, type KyooImage,
Movie, type Movie,
QueryIdentifier, type QueryIdentifier,
Show, type Show,
Studio, type Studio,
getDisplayDate, getDisplayDate,
queryFn, queryFn,
useAccount, useAccount,
} from "@kyoo/models"; } from "@kyoo/models";
import { WatchStatusV } from "@kyoo/models/src/resources/watch-status"; import type { WatchStatusV } from "@kyoo/models/src/resources/watch-status";
import { import {
A, A,
Chip, Chip,
@ -63,10 +63,10 @@ import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { Fragment } from "react"; import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ImageStyle, Platform, View } from "react-native"; import { type ImageStyle, Platform, View } from "react-native";
import { import {
Stylable, type Stylable,
Theme, type Theme,
em, em,
max, max,
md, md,

View File

@ -18,13 +18,13 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models"; import { type Movie, MovieP, type QueryIdentifier, type QueryPage } from "@kyoo/models";
import { usePageStyle } from "@kyoo/primitives";
import { Platform, ScrollView } from "react-native"; import { Platform, ScrollView } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { Header } from "./header";
import { DetailsCollections } from "./collection"; import { DetailsCollections } from "./collection";
import { usePageStyle } from "@kyoo/primitives"; import { Header } from "./header";
const query = (slug: string): QueryIdentifier<Movie> => ({ const query = (slug: string): QueryIdentifier<Movie> => ({
parser: MovieP, parser: MovieP,

View File

@ -19,7 +19,7 @@
*/ */
import { Avatar, Link, P, Skeleton, SubP } from "@kyoo/primitives"; import { Avatar, Link, P, Skeleton, SubP } from "@kyoo/primitives";
import { Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, useYoshiki } from "yoshiki/native";
export const PersonAvatar = ({ export const PersonAvatar = ({
slug, slug,

View File

@ -19,21 +19,21 @@
*/ */
import { import {
Episode, type Episode,
EpisodeP, EpisodeP,
QueryIdentifier, type QueryIdentifier,
Season, type Season,
SeasonP, SeasonP,
useInfiniteFetch, useInfiniteFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { Skeleton, H6, HR, P, ts, Menu, IconButton, tooltip, usePageStyle } from "@kyoo/primitives"; import { H6, HR, IconButton, Menu, P, Skeleton, tooltip, ts, usePageStyle } from "@kyoo/primitives";
import { rem, useYoshiki } from "yoshiki/native";
import { View } from "react-native";
import { InfiniteFetch } from "../fetch-infinite";
import { episodeDisplayNumber, EpisodeLine } from "./episode";
import { useTranslation } from "react-i18next";
import { ComponentType } from "react";
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg"; import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
import type { ComponentType } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { rem, useYoshiki } from "yoshiki/native";
import { InfiniteFetch } from "../fetch-infinite";
import { EpisodeLine, episodeDisplayNumber } from "./episode";
type SeasonProcessed = Season & { href: string }; type SeasonProcessed = Season & { href: string };

View File

@ -18,18 +18,24 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { QueryIdentifier, QueryPage, Show, ShowP, ShowWatchStatus } from "@kyoo/models"; import {
import { Platform, View, ViewProps } from "react-native"; type QueryIdentifier,
import { percent, useYoshiki } from "yoshiki/native"; type QueryPage,
import { DefaultLayout } from "../layout"; type Show,
import { EpisodeList, SeasonHeader } from "./season"; ShowP,
import { Header } from "./header"; type ShowWatchStatus,
import Svg, { Path, SvgProps } from "react-native-svg"; } from "@kyoo/models";
import { Container, H2, SwitchVariant, focusReset, ts } from "@kyoo/primitives"; import { Container, H2, SwitchVariant, focusReset, ts } from "@kyoo/primitives";
import { forwardRef, useState } from "react"; import { forwardRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View, type ViewProps } from "react-native";
import Svg, { Path, type SvgProps } from "react-native-svg";
import { percent, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout";
import { DetailsCollections } from "./collection"; import { DetailsCollections } from "./collection";
import { EpisodeLine, episodeDisplayNumber } from "./episode"; import { EpisodeLine, episodeDisplayNumber } from "./episode";
import { useTranslation } from "react-i18next"; import { Header } from "./header";
import { EpisodeList, SeasonHeader } from "./season";
export const SvgWave = (props: SvgProps) => { export const SvgWave = (props: SvgProps) => {
const { css } = useYoshiki(); const { css } = useYoshiki();

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { WatchInfo, getCurrentApiUrl, queryFn, toQueryKey } from "@kyoo/models"; import { type WatchInfo, getCurrentApiUrl, queryFn, toQueryKey } from "@kyoo/models";
import { Player } from "../player";
import { getCurrentAccount } from "@kyoo/models/src/account-internal"; import { getCurrentAccount } from "@kyoo/models/src/account-internal";
import { ReactNode } from "react"; import type { ReactNode } from "react";
import { Player } from "../player";
export const useDownloader = () => { export const useDownloader = () => {
return async (type: "episode" | "movie", slug: string) => { return async (type: "episode" | "movie", slug: string) => {

View File

@ -18,9 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { State, downloadAtom } from "./state"; import type { KyooImage } from "@kyoo/models";
import { FlashList } from "@shopify/flash-list";
import { ImageStyle, View } from "react-native";
import { import {
Alert, Alert,
H6, H6,
@ -34,17 +32,19 @@ import {
ts, ts,
usePageStyle, usePageStyle,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { EpisodeLine, displayRuntime, episodeDisplayNumber } from "../details/episode";
import { useTranslation } from "react-i18next";
import { EmptyView } from "../fetch";
import { percent, useYoshiki } from "yoshiki/native";
import { KyooImage } from "@kyoo/models";
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 { FlashList } from "@shopify/flash-list";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { type Atom, useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import { type ImageStyle, View } from "react-native";
import { percent, useYoshiki } from "yoshiki/native";
import { EpisodeLine, displayRuntime, episodeDisplayNumber } from "../details/episode";
import { EmptyView } from "../fetch";
import { type State, downloadAtom } from "./state";
const DownloadedItem = ({ const DownloadedItem = ({
name, name,
@ -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;
} }
}; };

View File

@ -21,27 +21,27 @@
import RNBackgroundDownloader, { import RNBackgroundDownloader, {
type DownloadTask, type DownloadTask,
} from "@kesha-antonov/react-native-background-downloader"; } from "@kesha-antonov/react-native-background-downloader";
import { deleteAsync } from "expo-file-system";
import { import {
Account, type Account,
Episode, type Episode,
EpisodeP, EpisodeP,
Movie, type Movie,
MovieP, MovieP,
QueryIdentifier, type QueryIdentifier,
WatchInfo, type WatchInfo,
WatchInfoP, WatchInfoP,
queryFn, queryFn,
toQueryKey, toQueryKey,
} from "@kyoo/models"; } from "@kyoo/models";
import { Player } from "../player";
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 { type QueryClient, useQueryClient } from "@tanstack/react-query";
import { Platform, ToastAndroid } from "react-native"; import { deleteAsync } from "expo-file-system";
import { QueryClient, useQueryClient } from "@tanstack/react-query"; import type { Router } from "expo-router/build/types";
import { Router } from "expo-router/build/types"; import { type PrimitiveAtom, atom, useSetAtom, useStore } from "jotai";
import { type ReactNode, useEffect } from "react";
import { ToastAndroid } from "react-native";
import { z } from "zod"; import { z } from "zod";
import { Player } from "../player";
export type State = { export type State = {
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED"; status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
@ -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({

View File

@ -20,13 +20,13 @@
import { ConnectionErrorContext, useAccount } from "@kyoo/models"; import { ConnectionErrorContext, useAccount } from "@kyoo/models";
import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives"; import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives";
import { useRouter } from "solito/router"; import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
import { useContext } from "react"; import { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useRouter } from "solito/router";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
export const ConnectionError = () => { export const ConnectionError = () => {
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -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

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ConnectionErrorContext, KyooErrors } from "@kyoo/models"; import { ConnectionErrorContext, type KyooErrors } from "@kyoo/models";
import { P } from "@kyoo/primitives"; import { P } from "@kyoo/primitives";
import { useContext, useLayoutEffect } from "react"; import { useContext, useLayoutEffect } from "react";
import { View } from "react-native"; import { View } from "react-native";
@ -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 (

View File

@ -18,13 +18,19 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointMap, HR } from "@kyoo/primitives"; import { HR, useBreakpointMap } from "@kyoo/primitives";
import { ContentStyle, FlashList } from "@shopify/flash-list"; import { type ContentStyle, FlashList } from "@shopify/flash-list";
import { ComponentProps, ComponentType, isValidElement, ReactElement, useRef } from "react"; import {
import { EmptyView, Layout, OfflineView, WithLoading, addHeader } from "./fetch"; type ComponentProps,
type ComponentType,
type ReactElement,
isValidElement,
useRef,
} from "react";
import { FlatList, View, type ViewStyle } from "react-native";
import { ErrorView } from "./errors"; import { ErrorView } from "./errors";
import { FlatList, View, ViewStyle } from "react-native"; import { EmptyView, type Layout, OfflineView, type WithLoading, addHeader } from "./fetch";
const emulateGap = ( const emulateGap = (
layout: "grid" | "vertical" | "horizontal", layout: "grid" | "vertical" | "horizontal",

View File

@ -18,22 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { HR } from "@kyoo/primitives"; import { HR } from "@kyoo/primitives";
import type { ContentStyle } from "@shopify/flash-list";
import { import {
ComponentProps, type ComponentProps,
ComponentType, type ComponentType,
Fragment, Fragment,
type ReactElement,
isValidElement, isValidElement,
ReactElement,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
} from "react"; } from "react";
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki"; import { type Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
import { EmptyView, Layout, WithLoading, addHeader } from "./fetch";
import { ErrorView } from "./errors"; import { ErrorView } from "./errors";
import type { ContentStyle } from "@shopify/flash-list"; import { EmptyView, type Layout, type WithLoading, addHeader } from "./fetch";
const InfiniteScroll = <Props,>({ const InfiniteScroll = <Props,>({
children, children,
@ -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",

View File

@ -18,9 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Page, QueryIdentifier, useFetch } from "@kyoo/models"; import { type Page, type QueryIdentifier, useFetch } from "@kyoo/models";
import { Breakpoint, P } from "@kyoo/primitives"; import { type Breakpoint, P } from "@kyoo/primitives";
import { ComponentType, ReactElement, isValidElement } from "react"; import { type ComponentType, type ReactElement, isValidElement } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
@ -37,7 +37,7 @@ export type WithLoading<Item> =
| (Item & { isLoading: false }) | (Item & { isLoading: false })
| (Partial<Item> & { isLoading: true }); | (Partial<Item> & { isLoading: true });
const isPage = <T = unknown,>(obj: unknown): obj is Page<T> => const isPage = <T = unknown>(obj: unknown): obj is Page<T> =>
(typeof obj === "object" && obj && "items" in obj) || false; (typeof obj === "object" && obj && "items" in obj) || false;
export const Fetch = <Data,>({ export const Fetch = <Data,>({

Some files were not shown because too many files have changed in this diff Show More