diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index e43a8d28..33d83466 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -26,10 +26,6 @@ import { Poppins_900Black, useFonts, } from "@expo-google-fonts/poppins"; -import "@formatjs/intl-displaynames/locale-data/en"; -import "@formatjs/intl-displaynames/locale-data/fr"; -import "@formatjs/intl-displaynames/polyfill"; -import "@formatjs/intl-locale/polyfill"; import { PortalProvider } from "@gorhom/portal"; import { AccountProvider, createQueryClient, storage, useUserTheme } from "@kyoo/models"; import { SnackbarProvider, ThemeSelector } from "@kyoo/primitives"; @@ -45,14 +41,11 @@ import "intl-pluralrules"; import { type ReactNode, useEffect, useState } from "react"; import { initReactI18next } from "react-i18next"; import { useColorScheme } from "react-native"; +import resources from "../../../translations"; import NetInfo from "@react-native-community/netinfo"; import { onlineManager } from "@tanstack/react-query"; import { useTheme } from "yoshiki/native"; -// TODO: use a backend to load jsons. -import en from "../../../translations/en.json"; -import fr from "../../../translations/fr.json"; -import zh from "../../../translations/zh.json"; onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { @@ -81,11 +74,7 @@ i18next.use(initReactI18next).init({ }, fallbackLng: "en", lng: getLocales()[0].languageCode ?? "en", - resources: { - en: { translation: en }, - fr: { translation: fr }, - zh: { translation: zh }, - }, + resources, }); const NavigationThemeProvider = ({ children }: { children: ReactNode }) => { diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json index f15d2376..370bf2af 100644 --- a/front/apps/mobile/package.json +++ b/front/apps/mobile/package.json @@ -15,58 +15,57 @@ }, "dependencies": { "@expo-google-fonts/poppins": "^0.2.3", - "@formatjs/intl-displaynames": "^6.6.6", - "@formatjs/intl-locale": "^3.4.5", + "@formatjs/intl-displaynames": "^6.6.8", + "@formatjs/intl-locale": "^4.0.0", "@gorhom/portal": "^1.0.14", - "@kesha-antonov/react-native-background-downloader": "git+https://github.com/zoriya/react-native-background-downloader.git", + "@kesha-antonov/react-native-background-downloader": "^3.1.3", "@kyoo/ui": "workspace:^", - "@material-symbols/svg-400": "^0.14.6", - "@react-native-community/netinfo": "11.1.0", - "@shopify/flash-list": "1.6.3", - "@tanstack/query-sync-storage-persister": "^5.17.19", - "@tanstack/react-query": "^5.17.19", - "@tanstack/react-query-persist-client": "^5.17.19", + "@material-symbols/svg-400": "^0.18.0", + "@react-native-community/netinfo": "11.3.1", + "@shopify/flash-list": "1.6.4", + "@tanstack/query-sync-storage-persister": "^5.37.1", + "@tanstack/react-query": "^5.37.1", + "@tanstack/react-query-persist-client": "^5.37.1", "array-shuffle": "^3.0.0", "babel-plugin-transform-inline-environment-variables": "^0.4.4", - "expo": "^50.0.4", - "expo-build-properties": "~0.11.0", - "expo-constants": "~15.4.5", - "expo-dev-client": "~3.3.7", - "expo-file-system": "~16.0.5", - "expo-font": "~11.10.2", - "expo-image-picker": "~14.7.1", - "expo-linear-gradient": "~12.7.1", - "expo-linking": "~6.2.2", - "expo-localization": "~14.8.3", - "expo-navigation-bar": "~2.8.1", - "expo-router": "3.4.6", - "expo-screen-orientation": "~6.4.1", - "expo-secure-store": "~12.8.1", - "expo-status-bar": "~1.11.1", - "expo-updates": "~0.24.9", - "i18next": "^23.7.20", + "expo": "^51.0.8", + "expo-build-properties": "~0.12.1", + "expo-constants": "~16.0.1", + "expo-dev-client": "~4.0.14", + "expo-file-system": "~17.0.1", + "expo-font": "~12.0.5", + "expo-image-picker": "~15.0.5", + "expo-linear-gradient": "~13.0.2", + "expo-linking": "~6.3.1", + "expo-localization": "~15.0.3", + "expo-navigation-bar": "~3.0.4", + "expo-router": "3.5.14", + "expo-screen-orientation": "~7.0.5", + "expo-secure-store": "~13.0.1", + "expo-status-bar": "~1.12.1", + "expo-updates": "~0.25.14", + "i18next": "^23.11.4", "intl-pluralrules": "^2.0.1", - "moti": "^0.27.2", + "moti": "^0.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-i18next": "^14.0.1", - "react-native": "0.73.2", - "react-native-blurhash": "^1.1.11", + "react-i18next": "^14.1.1", + "react-native": "0.74.1", + "react-native-blurhash": "^2.0.2", "react-native-fast-image": "^8.6.3", - "react-native-mmkv": "^2.11.0", - "react-native-reanimated": "~3.6.2", - "react-native-safe-area-context": "4.8.2", - "react-native-screens": "~3.29.0", - "react-native-svg": "14.1.0", - "react-native-uuid": "^2.0.1", - "react-native-video": "^6.0.0-beta.4", + "react-native-mmkv": "^2.12.2", + "react-native-reanimated": "~3.10.1", + "react-native-safe-area-context": "4.10.1", + "react-native-screens": "~3.31.1", + "react-native-svg": "15.2.0", + "react-native-uuid": "^2.0.2", + "react-native-video": "^6.0.0", "yoshiki": "1.2.14" }, "devDependencies": { - "@babel/core": "^7.23.9", - "@types/react": "18.2.48", - "react-native-svg-transformer": "^1.3.0", - "typescript": "^5.3.3" + "@babel/core": "^7.24.5", + "react-native-svg-transformer": "^1.4.0", + "typescript": "~5.3.3" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 8388d969..97e71a95 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -16,46 +16,45 @@ "@kyoo/models": "workspace:^", "@kyoo/primitives": "workspace:^", "@kyoo/ui": "workspace:^", - "@material-symbols/svg-400": "^0.14.6", + "@material-symbols/svg-400": "^0.18.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-select": "^2.0.0", - "@tanstack/react-query": "^5.17.19", - "@tanstack/react-query-devtools": "^5.17.21", + "@tanstack/react-query": "^5.37.1", + "@tanstack/react-query-devtools": "^5.37.1", "array-shuffle": "^3.0.0", - "expo-image-picker": "~14.7.1", - "expo-linear-gradient": "^12.7.1", - "expo-modules-core": "^1.11.8", - "hls.js": "^1.5.6", - "i18next": "^23.7.20", + "expo-image-picker": "~15.0.5", + "expo-linear-gradient": "^13.0.2", + "expo-modules-core": "^1.12.11", + "hls.js": "^1.5.8", + "i18next": "^23.11.4", "jassub": "^1.7.15", - "jotai": "^2.6.3", - "moti": "^0.27.2", - "next": "14.1.0", + "jotai": "^2.8.0", + "moti": "^0.29.0", + "next": "14.2.3", "next-translate": "^2.6.2", "raf": "^3.4.1", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-i18next": "^14.0.1", - "react-native-reanimated": "3.6.2", - "react-native-svg": "14.1.0", - "react-native-video": "^6.0.0-beta.4", - "react-native-web": "0.19.10", - "react-tooltip": "^5.26.0", - "solito": "^4.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-i18next": "^14.1.1", + "react-native-reanimated": "3.11.0", + "react-native-svg": "15.3.0", + "react-native-video": "^6.0.0", + "react-native-web": "0.19.11", + "react-tooltip": "^5.26.4", + "solito": "^4.2.2", "srt-webvtt": "zoriya/srt-webvtt#build", "superjson": "^2.2.1", - "sweetalert2": "^11.10.4", + "sweetalert2": "^11.11.0", "yoshiki": "1.2.14", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@svgr/webpack": "^8.1.0", - "@types/node": "20.11.7", - "@types/react": "18.2.48", - "@types/react-dom": "18.2.18", + "@types/node": "20.12.12", + "@types/react-dom": "18.3.0", "copy-webpack-plugin": "^12.0.2", - "react-native": "0.73.2", - "typescript": "^5.3.3", - "webpack": "^5.90.0" + "react-native": "0.74.1", + "typescript": "^5.4.5", + "webpack": "^5.91.0" } } diff --git a/front/apps/web/src/i18n.tsx b/front/apps/web/src/i18n.tsx index 418e6604..cf692d44 100644 --- a/front/apps/web/src/i18n.tsx +++ b/front/apps/web/src/i18n.tsx @@ -22,10 +22,7 @@ import i18next, { type InitOptions } from "i18next"; import type { AppContext, AppInitialProps, AppProps } from "next/app"; import { type ComponentType, useMemo } from "react"; import { I18nextProvider } from "react-i18next"; - -import en from "../../../translations/en.json"; -import fr from "../../../translations/fr.json"; -import zh from "../../../translations/zh.json"; +import resources from "../../../translations"; export const withTranslations = ( AppToTranslate: ComponentType & { @@ -37,6 +34,7 @@ export const withTranslations = ( interpolation: { escapeValue: false, }, + resources, }; const AppWithTranslations = (props: AppProps) => { @@ -45,10 +43,10 @@ export const withTranslations = ( i18next.init({ ...commonOptions, lng: props.pageProps.__lang, - resources: props.pageProps.__resources, + fallbackLng: "en", }); return i18next; - }, [props.pageProps.__lang, props.pageProps.__resources]); + }, [props.pageProps.__lang]); return ( @@ -59,21 +57,12 @@ export const withTranslations = ( AppWithTranslations.getInitialProps = async (ctx: AppContext) => { const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx); const lng = ctx.router.locale || ctx.router.defaultLocale || "en"; - // TODO: use a backend to fetch only the needed translations. - // TODO: use a different backend on the client and fetch needed translations. - const resources = { - en: { translation: en }, - fr: { translation: fr }, - zh: { translation: zh }, - }; await i18n.init({ ...commonOptions, lng, - fallbackLng: ctx.router.defaultLocale || "en", - resources, + fallbackLng: "en", }); props.pageProps.__lang = lng; - props.pageProps.__resources = resources; return props; }; diff --git a/front/package.json b/front/package.json index 346c815b..1922a89a 100644 --- a/front/package.json +++ b/front/package.json @@ -17,7 +17,8 @@ "workspaces": ["apps/*", "packages/*"], "devDependencies": { "@biomejs/biome": "1.7.3", - "typescript": "5.3.3" + "@types/react": "~18.2.79", + "typescript": "5.4.5" }, "packageManager": "yarn@3.2.4" } diff --git a/front/packages/models/package.json b/front/packages/models/package.json index b866576e..c5ba651f 100644 --- a/front/packages/models/package.json +++ b/front/packages/models/package.json @@ -5,9 +5,8 @@ "sideEffects": false, "packageManager": "yarn@3.2.4", "devDependencies": { - "@types/react": "18.2.48", - "react-native-mmkv": "^2.11.0", - "typescript": "^5.3.3" + "react-native-mmkv": "^2.12.2", + "typescript": "^5.4.5" }, "peerDependencies": { "@tanstack/react-query": "*", @@ -20,6 +19,6 @@ } }, "dependencies": { - "zod": "^3.22.4" + "zod": "^3.23.8" } } diff --git a/front/packages/models/src/accounts.tsx b/front/packages/models/src/accounts.tsx index 5dbb98d4..f2071a21 100644 --- a/front/packages/models/src/accounts.tsx +++ b/front/packages/models/src/accounts.tsx @@ -133,19 +133,31 @@ export const AccountProvider = ({ setApiUrl(selected?.apiUrl ?? defaultApiUrl); }, [selected, setApiUrl]); - const user = useFetch({ + const { + isSuccess: userIsSuccess, + isError: userIsError, + isLoading: userIsLoading, + isPlaceholderData: userIsPlaceholder, + data: user, + error: userError, + } = useFetch({ path: ["auth", "me"], parser: UserP, placeholderData: selected as User, enabled: !!selected, }); + // Use a ref here because we don't want the effect to trigger when the selected + // value has changed, only when the fetch result changed + // If we trigger the effect when the selected value change, we enter an infinite render loop + const selectedRef = useRef(selected); + selectedRef.current = selected; useEffect(() => { - if (!selected || !user.isSuccess || user.isPlaceholderData) return; + if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return; // The id is different when user is stale data, we need to wait for the use effect to invalidate the query. - if (user.data.id !== selected.id) return; - const nUser = { ...selected, ...user.data }; - if (!Object.is(selected, nUser)) updateAccount(nUser.id, nUser); - }, [selected, user]); + if (user.id !== selectedRef.current.id) return; + const nUser = { ...selectedRef.current, ...user }; + updateAccount(nUser.id, nUser); + }, [user, userIsSuccess, userIsPlaceholder]); const queryClient = useQueryClient(); const oldSelected = useRef<{ id: string; token: string } | null>( @@ -154,7 +166,6 @@ export const AccountProvider = ({ const [permissionError, setPermissionError] = useState(null); - const userIsError = user.isError; useEffect(() => { // if the user change account (or connect/disconnect), reset query cache. if ( @@ -180,8 +191,8 @@ export const AccountProvider = ({ { queryClient.invalidateQueries({ queryKey: ["auth", "me"] }); }, diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index c39978c6..ec4f4937 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -33,6 +33,12 @@ import { type Page, Paged } from "./page"; export let lastUsedUrl: string = null!; +const cleanSlash = (str: string | null, keepFirst = false) => { + if (!str) return null; + if (keepFirst) return str.replace(/\/$/g, ""); + return str.replace(/^\/|\/$/g, ""); +}; + export const queryFn = async ( context: { apiUrl?: string | null; @@ -54,17 +60,16 @@ export const queryFn = async ( lastUsedUrl = url!; const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken; - const path = [url] + const path = [cleanSlash(url, true)] .concat( "path" in context ? (context.path as string[]) : "pageParam" in context && context.pageParam - ? [context.pageParam as string] + ? [cleanSlash(context.pageParam as string)] : (context.queryKey as string[]), ) .filter((x) => x) .join("/") - .replace("//", "/") .replace("/?", "?"); let resp: Response; try { diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts index 82e624bd..a0a10d86 100644 --- a/front/packages/models/src/resources/watch-info.ts +++ b/front/packages/models/src/resources/watch-info.ts @@ -18,21 +18,10 @@ * along with Kyoo. If not, see . */ -import i18next from "i18next"; import { z } from "zod"; import { imageFn } from "../traits"; import { QualityP } from "./quality"; -const getDisplayName = (sub: Track) => { - const languageNames = new Intl.DisplayNames([i18next.language ?? "en"], { type: "language" }); - const lng = sub.language ? languageNames.of(sub.language) : undefined; - - if (lng && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`; - if (lng) return lng; - if (sub.title) return sub.title; - return `Unknown (${sub.index})`; -}; - /** * A Video track */ @@ -97,10 +86,7 @@ export const TrackP = z.object({ }); export type Track = z.infer; -export const AudioP = TrackP.transform((x) => ({ - ...x, - displayName: getDisplayName(x), -})); +export const AudioP = TrackP; export type Audio = z.infer; export const SubtitleP = TrackP.extend({ @@ -108,10 +94,7 @@ export const SubtitleP = TrackP.extend({ * The url of this track (only if this is a subtitle).. */ link: z.string().transform(imageFn).nullable(), -}).transform((x) => ({ - ...x, - displayName: getDisplayName(x), -})); +}); export type Subtitle = z.infer; export const ChapterP = z.object({ diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json index b51482b6..4f390639 100644 --- a/front/packages/primitives/package.json +++ b/front/packages/primitives/package.json @@ -6,8 +6,7 @@ "packageManager": "yarn@3.2.4", "devDependencies": { "@gorhom/portal": "^1.0.14", - "@types/react": "18.2.48", - "typescript": "^5.3.3" + "typescript": "^5.4.5" }, "peerDependencies": { "@gorhom/portal": "*", @@ -53,15 +52,15 @@ } }, "dependencies": { - "@expo/html-elements": "^0.9.1", - "@tanstack/react-query": "^5.17.19", - "solito": "^4.2.0" + "@expo/html-elements": "^0.10.1", + "@tanstack/react-query": "^5.37.1", + "solito": "^4.2.2" }, "optionalDependencies": { "@radix-ui/react-select": "^2.0.0", "blurhash": "^2.0.5", - "react-native-blurhash": "^1.1.11", + "react-native-blurhash": "^2.0.2", "react-native-fast-image": "^8.6.3", - "react-native-safe-area-context": "4.8.2" + "react-native-safe-area-context": "4.10.1" } } diff --git a/front/packages/primitives/src/menu.tsx b/front/packages/primitives/src/menu.tsx index 9e255dba..90f05320 100644 --- a/front/packages/primitives/src/menu.tsx +++ b/front/packages/primitives/src/menu.tsx @@ -68,7 +68,7 @@ const Menu = ({ const [isOpen, setOpen] = outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false); - // deos the same as a useMemo but for props. + // does the same as a useMemo but for props. const memoRef = useRef({ onMenuOpen, onMenuClose }); memoRef.current = { onMenuOpen, onMenuClose }; useEffect(() => { diff --git a/front/packages/primitives/src/select.tsx b/front/packages/primitives/src/select.tsx index e6fc194d..8c21d27b 100644 --- a/front/packages/primitives/src/select.tsx +++ b/front/packages/primitives/src/select.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg"; +import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg"; import { Button } from "./button"; import { Icon } from "./icons"; import { Menu } from "./menu"; diff --git a/front/packages/primitives/src/select.web.tsx b/front/packages/primitives/src/select.web.tsx index 0c235d9e..6aa8a1b6 100644 --- a/front/packages/primitives/src/select.web.tsx +++ b/front/packages/primitives/src/select.web.tsx @@ -19,8 +19,8 @@ */ import Check from "@material-symbols/svg-400/rounded/check-fill.svg"; -import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg"; -import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg"; +import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg"; +import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg"; import * as RSelect from "@radix-ui/react-select"; import { forwardRef } from "react"; import { View } from "react-native"; diff --git a/front/packages/primitives/src/themes/theme.tsx b/front/packages/primitives/src/themes/theme.tsx index b31d1eea..ffc764ce 100644 --- a/front/packages/primitives/src/themes/theme.tsx +++ b/front/packages/primitives/src/themes/theme.tsx @@ -20,7 +20,7 @@ import type { Property } from "csstype"; import type { ReactNode } from "react"; -import { Platform } from "react-native"; +import { Platform, type TextStyle } from "react-native"; import { type Theme, ThemeProvider, useAutomaticTheme } from "yoshiki"; import "yoshiki"; import { useTheme, useYoshiki } from "yoshiki/native"; @@ -28,10 +28,7 @@ import "yoshiki/native"; import { catppuccin } from "./catppuccin"; type FontList = Partial< - Record< - "normal" | "bold" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900", - string - > + Record, string> >; type Mode = { @@ -150,7 +147,7 @@ export const ThemeSelector = ({ }) => { const newTheme = selectMode({ ...catppuccin, font }, theme); - return {children}; + return {children as any}; }; export type YoshikiFunc = (props: ReturnType) => T; @@ -165,7 +162,11 @@ export const SwitchVariant = ({ children }: { children: ReactNode | YoshikiFunc< return ( - {typeof children === "function" ? {children} : children} + {typeof children === "function" ? ( + {children} + ) : ( + (children as any) + )} ); }; @@ -197,7 +198,11 @@ export const ContrastArea = ({ : theme } > - {typeof children === "function" ? {children} : children} + {typeof children === "function" ? ( + {children} + ) : ( + (children as any) + )} ); }; diff --git a/front/packages/ui/package.json b/front/packages/ui/package.json index aa9a0bc8..50686bed 100644 --- a/front/packages/ui/package.json +++ b/front/packages/ui/package.json @@ -5,14 +5,15 @@ "packageManager": "yarn@3.2.4", "dependencies": { "@kyoo/models": "workspace:^", - "@kyoo/primitives": "workspace:^" + "@kyoo/primitives": "workspace:^", + "langmap": "^0.0.16" }, "devDependencies": { "@gorhom/portal": "^1.0.14", - "@shopify/flash-list": "^1.6.3", - "@types/react": "18.2.48", - "react-native-uuid": "^2.0.1", - "typescript": "^5.3.3" + "@shopify/flash-list": "^1.6.4", + "@types/langmap": "^0.0.3", + "react-native-uuid": "^2.0.2", + "typescript": "^5.4.5" }, "peerDependencies": { "@gorhom/portal": "*", @@ -34,9 +35,9 @@ "yoshiki": "*" }, "optionalDependencies": { - "@kesha-antonov/react-native-background-downloader": "git+https://github.com/zoriya/react-native-background-downloader.git", - "expo-file-system": "^16.0.5", - "expo-router": "^3.4.6" + "@kesha-antonov/react-native-background-downloader": "^3.1.3", + "expo-file-system": "^17.0.1", + "expo-router": "^3.5.14" }, "peerDependenciesMeta": { "@kesha-antonov/react-native-background-downloader": { diff --git a/front/packages/ui/src/browse/grid.tsx b/front/packages/ui/src/browse/grid.tsx index 648dc136..cf708683 100644 --- a/front/packages/ui/src/browse/grid.tsx +++ b/front/packages/ui/src/browse/grid.tsx @@ -30,7 +30,7 @@ import { important, ts, } from "@kyoo/primitives"; -import Done from "@material-symbols/svg-400/rounded/done-fill.svg"; +import Done from "@material-symbols/svg-400/rounded/check-fill.svg"; import { useState } from "react"; import { type ImageStyle, Platform, View } from "react-native"; import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native"; diff --git a/front/packages/ui/src/components/context-menus.tsx b/front/packages/ui/src/components/context-menus.tsx index af9e6673..ea2904b6 100644 --- a/front/packages/ui/src/components/context-menus.tsx +++ b/front/packages/ui/src/components/context-menus.tsx @@ -95,7 +95,7 @@ export const EpisodesContext = ({ {Object.values(WatchStatusV).map((x) => ( }`)} onSelect={() => mutation.mutate(x)} selected={x === status} /> diff --git a/front/packages/ui/src/components/media-info.tsx b/front/packages/ui/src/components/media-info.tsx index ae7d9099..77b2dc65 100644 --- a/front/packages/ui/src/components/media-info.tsx +++ b/front/packages/ui/src/components/media-info.tsx @@ -31,16 +31,19 @@ import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { useYoshiki } from "yoshiki/native"; import { Fetch } from "../fetch"; +import { useDisplayName } from "../utils"; const MediaInfoTable = ({ mediaInfo: { path, video, container, audios, subtitles, duration, size }, }: { mediaInfo: Partial; }) => { + const getDisplayName = useDisplayName(); const { t } = useTranslation(); const { css } = useYoshiki(); + const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`; - const formatTrackTable = (trackTable: (Audio | Subtitle)[], s: string) => { + const formatTrackTable = (trackTable: (Audio | Subtitle)[], type: "subtitles" | "audio") => { if (trackTable.length === 0) { return undefined; } @@ -48,15 +51,16 @@ const MediaInfoTable = ({ return trackTable.reduce( (collected, audioTrack, index) => { // If there is only one track, we do not need to show an index - collected[singleTrack ? t(s) : `${t(s)} ${index + 1}`] = [ - audioTrack.displayName, - // Only show it if there is more than one track - audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined, - audioTrack.isForced ? t("mediainfo.forced") : undefined, - audioTrack.codec, - ] - .filter((x) => x !== undefined) - .join(" - "); + collected[singleTrack ? t(`mediainfo.${type}`) : `${t(`mediainfo.${type}`)} ${index + 1}`] = + [ + getDisplayName(audioTrack), + // Only show it if there is more than one track + audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined, + audioTrack.isForced ? t("mediainfo.forced") : undefined, + audioTrack.codec, + ] + .filter((x) => x !== undefined) + .join(" - "); return collected; }, {} as Record, @@ -81,10 +85,10 @@ const MediaInfoTable = ({ }, audios === undefined ? { [t("mediainfo.audio")]: undefined } - : formatTrackTable(audios, "mediainfo.audio"), + : formatTrackTable(audios, "audio"), subtitles === undefined ? { [t("mediainfo.subtitles")]: undefined } - : formatTrackTable(subtitles, "mediainfo.subtitles"), + : formatTrackTable(subtitles, "subtitles"), ] as const ).filter((x) => x !== undefined) as Record[]; return ( diff --git a/front/packages/ui/src/details/episode.tsx b/front/packages/ui/src/details/episode.tsx index fe928b4c..d9d0af7b 100644 --- a/front/packages/ui/src/details/episode.tsx +++ b/front/packages/ui/src/details/episode.tsx @@ -33,8 +33,8 @@ import { tooltip, ts, } from "@kyoo/primitives"; -import ExpandLess from "@material-symbols/svg-400/rounded/expand_less-fill.svg"; -import ExpandMore from "@material-symbols/svg-400/rounded/expand_more-fill.svg"; +import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg"; +import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { type ImageStyle, Platform, type PressableProps, View } from "react-native"; diff --git a/front/packages/ui/src/downloads/state.tsx b/front/packages/ui/src/downloads/state.tsx index d463293e..c1d8b503 100644 --- a/front/packages/ui/src/downloads/state.tsx +++ b/front/packages/ui/src/downloads/state.tsx @@ -36,15 +36,17 @@ import { import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal"; import { type QueryClient, useQueryClient } from "@tanstack/react-query"; import { deleteAsync } from "expo-file-system"; -import type { Router } from "expo-router/build/types"; +import type { useRouter } from "expo-router"; import { type PrimitiveAtom, atom, useSetAtom, useStore } from "jotai"; import { type ReactNode, useEffect } from "react"; import { ToastAndroid } from "react-native"; import { z } from "zod"; import { Player } from "../player"; +type Router = ReturnType; + export type State = { - status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED"; + status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED" | "PENDING"; progress: number | null; size: number; availableSize: number; @@ -190,7 +192,7 @@ const download = ( headers: { Authorization: account.token.access_token, }, - showNotification: true, + isNotificationVisible: true, // TODO: Implement only wifi // network: Network.ALL, }); diff --git a/front/apps/web/src/i18n-d.ts b/front/packages/ui/src/i18n-d.d.ts similarity index 95% rename from front/apps/web/src/i18n-d.ts rename to front/packages/ui/src/i18n-d.d.ts index 46c3f118..4d963c6e 100644 --- a/front/apps/web/src/i18n-d.ts +++ b/front/packages/ui/src/i18n-d.d.ts @@ -24,6 +24,6 @@ import type en from "../../../translations/en.json"; declare module "i18next" { interface CustomTypeOptions { returnNull: false; - resources: { translations: typeof en }; + resources: { translation: typeof en }; } } diff --git a/front/packages/ui/src/i18n-d.ts b/front/packages/ui/src/i18n-d.ts deleted file mode 100644 index 46c3f118..00000000 --- a/front/packages/ui/src/i18n-d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import "i18next"; -import type en from "../../../translations/en.json"; - -declare module "i18next" { - interface CustomTypeOptions { - returnNull: false; - resources: { translations: typeof en }; - } -} diff --git a/front/packages/ui/src/login/server-url.tsx b/front/packages/ui/src/login/server-url.tsx index a2b6ad33..74a65329 100644 --- a/front/packages/ui/src/login/server-url.tsx +++ b/front/packages/ui/src/login/server-url.tsx @@ -48,7 +48,7 @@ const query: QueryIdentifier = { export const ServerUrlPage: QueryPage = () => { const [_apiUrl, setApiUrl] = useState(""); const apiUrl = cleanApiUrl(_apiUrl); - const { data, error } = useFetch({ ...query, options: { apiUrl } }); + const { data, error } = useFetch({ ...query, options: { apiUrl, authenticated: false } }); const router = useRouter(); const { t } = useTranslation(); const { css } = useYoshiki(); diff --git a/front/packages/ui/src/player/components/hover.tsx b/front/packages/ui/src/player/components/hover.tsx index db0f8b45..b4170e6f 100644 --- a/front/packages/ui/src/player/components/hover.tsx +++ b/front/packages/ui/src/player/components/hover.tsx @@ -295,6 +295,7 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { playerWidth.current = e.nativeEvent.layout.width; }} {...css( + // @ts-expect-error Web only property (cursor: unset) { flexDirection: "row", justifyContent: "center", @@ -304,7 +305,6 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => { left: 0, right: 0, bottom: 0, - // @ts-expect-error Web only property cursor: hover ? "unset" : "none", }, props, diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx index 28ecb01f..32c6a191 100644 --- a/front/packages/ui/src/player/components/right-buttons.tsx +++ b/front/packages/ui/src/player/components/right-buttons.tsx @@ -29,6 +29,7 @@ import { useAtom } from "jotai"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; import { type Stylable, useYoshiki } from "yoshiki/native"; +import { useDisplayName } from "../../utils"; import { fullscreenAtom, subtitleAtom } from "../state"; import { AudiosMenu, QualitiesMenu } from "../video"; @@ -48,6 +49,7 @@ export const RightButtons = ({ } & Stylable) => { const { css } = useYoshiki(); const { t } = useTranslation(); + const getDisplayName = useDisplayName(); const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom); const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom); @@ -72,7 +74,7 @@ export const RightButtons = ({ {subtitles.map((x) => ( setSubtitle(x)} diff --git a/front/packages/ui/src/player/index.tsx b/front/packages/ui/src/player/index.tsx index bd496e29..df35889f 100644 --- a/front/packages/ui/src/player/index.tsx +++ b/front/packages/ui/src/player/index.tsx @@ -39,7 +39,6 @@ import { episodeDisplayNumber } from "../details/episode"; import { ErrorView } from "../errors"; import { Back, Hover, LoadingIndicator } from "./components/hover"; import { useVideoKeyboard } from "./keyboard"; -import { MediaSessionManager } from "./media-session"; import { Video, durationAtom, fullscreenAtom } from "./state"; import { WatchStatusObserver } from "./watch-status-observer"; @@ -90,6 +89,15 @@ export const Player = ({ data && data.type === "episode" && data.nextEpisode ? `/watch/${data.nextEpisode.slug}?t=0` : undefined; + const title = + data && + (data.type === "movie" + ? data.name + : `${data.show!.name} ${episodeDisplayNumber({ + seasonNumber: data.seasonNumber, + episodeNumber: data.episodeNumber, + absoluteNumber: data.absoluteNumber, + })}`); useVideoKeyboard(info?.subtitles, info?.fonts, previous, next); @@ -119,26 +127,7 @@ export const Player = ({ return ( <> - {data && ( - - )} - + {data && info && ( )} @@ -150,6 +139,13 @@ export const Player = ({ })} >