diff --git a/front/apps/mobile/app/(app)/search/index.tsx b/front/apps/mobile/app/(app)/search/index.tsx index 4c0d1c88..ad3064f3 100644 --- a/front/apps/mobile/app/(app)/search/index.tsx +++ b/front/apps/mobile/app/(app)/search/index.tsx @@ -22,7 +22,7 @@ import { SearchPage } from "@kyoo/ui"; import { Stack, useLocalSearchParams } from "expo-router"; import { useTranslation } from "react-i18next"; import { createParam } from "solito"; -import { useRouter } from "solito/router"; +import { useRouter } from "@kyoo/primitives"; import { useTheme } from "yoshiki/native"; const { useParam } = createParam<{ q?: string }>(); diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json index b51482b6..a75c4a39 100644 --- a/front/packages/primitives/package.json +++ b/front/packages/primitives/package.json @@ -54,8 +54,7 @@ }, "dependencies": { "@expo/html-elements": "^0.9.1", - "@tanstack/react-query": "^5.17.19", - "solito": "^4.2.0" + "@tanstack/react-query": "^5.17.19" }, "optionalDependencies": { "@radix-ui/react-select": "^2.0.0", diff --git a/front/packages/primitives/src/index.ts b/front/packages/primitives/src/index.ts index daec9bd5..c163e34f 100644 --- a/front/packages/primitives/src/index.ts +++ b/front/packages/primitives/src/index.ts @@ -42,3 +42,4 @@ export * from "./chip"; export * from "./utils"; export * from "./constants"; +export * from "./navigation/router"; diff --git a/front/packages/primitives/src/links.tsx b/front/packages/primitives/src/links.tsx index 466463fa..b20c6f73 100644 --- a/front/packages/primitives/src/links.tsx +++ b/front/packages/primitives/src/links.tsx @@ -18,19 +18,10 @@ * along with Kyoo. If not, see . */ -import type { UrlObject } from "node:url"; -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 { parseNextPath } from "solito/router"; +import { forwardRef, type ReactNode } from "react"; +import { Platform, Pressable, type TextProps, type View, type PressableProps } from "react-native"; import { useTheme, useYoshiki } from "yoshiki/native"; +import type { UrlObject } from "node:url"; import { alpha } from "./themes"; export const A = ({ @@ -122,9 +113,7 @@ export const Link = ({ {...props} onPress={(e?: any) => { props?.onPress?.(e); - if (e?.defaultPrevented) return; - if (Platform.OS !== "web" && href?.includes("://")) Linking.openURL(href); - else linkProps.onPress(e); + linkProps.onPress(e); }} > {children} diff --git a/front/packages/primitives/src/menu.tsx b/front/packages/primitives/src/menu.tsx index 9e255dba..08fb17af 100644 --- a/front/packages/primitives/src/menu.tsx +++ b/front/packages/primitives/src/menu.tsx @@ -39,6 +39,7 @@ import { useRouter } from "solito/router"; import { percent, px, sm, useYoshiki, vh, xl } from "yoshiki/native"; import { Icon, IconButton } from "./icons"; import { PressableFeedback } from "./links"; +import { useRouter } from "./navigation/router"; import { P } from "./text"; import { ContrastArea, SwitchVariant } from "./themes"; import { ts } from "./utils"; diff --git a/front/packages/primitives/src/navigation/link.ts b/front/packages/primitives/src/navigation/link.ts new file mode 100644 index 00000000..3825671f --- /dev/null +++ b/front/packages/primitives/src/navigation/link.ts @@ -0,0 +1,44 @@ +import { type UrlObject, format } from "node:url"; +import type { MouseEvent } from "react"; +import { Platform, type PressableProps, Linking } from "react-native"; +import { useRouter } from "./router"; + +export const useLink = ( + route: string | UrlObject, + opts: { replace?: boolean; target?: "_blank"; isNested?: boolean } = {}, +) => { + const router = useRouter(); + const href = typeof route === "object" ? format(route) : route; + + return { + accessibilityRole: "link", + href, + onPress: (e) => { + if (e?.defaultPrevented) return; + if (Platform.OS !== "web" && href.includes("://")) { + Linking.openURL(href); + return; + } + + if (Platform.OS === "web" && e) { + // Web event + const we = e as unknown as MouseEvent; + if ( + // ignore clicks with modifier keys + we.metaKey || + we.altKey || + we.ctrlKey || + we.shiftKey || + // ignore everything but left clicks + we.button !== null || + we.button !== 0 + ) + return; + } + + e.preventDefault(); + if (opts.replace === true) router.replace(href, { isNested: opts.isNested }); + else router.push(href); + }, + } satisfies PressableProps & { href?: string }; +}; diff --git a/front/packages/primitives/src/navigation/router.ts b/front/packages/primitives/src/navigation/router.ts new file mode 100644 index 00000000..b70e4c69 --- /dev/null +++ b/front/packages/primitives/src/navigation/router.ts @@ -0,0 +1,18 @@ +import { navigate } from "vike/client/router"; +import { type UrlObject, format } from "node:url"; + +export const useRouter = () => { + return { + push: (route: string | UrlObject) => { + if (typeof route === "object") route = format(route); + navigate(route); + }, + replace: (route: string | UrlObject, opts: {isNested?: boolean} = {}) => { + if (typeof route === "object") route = format(route); + navigate(route, { overwriteLastHistoryEntry: opts.isNested }); + }, + back: () => { + window.history.back(); + }, + }; +}; diff --git a/front/packages/primitives/src/navigation/router.web.ts b/front/packages/primitives/src/navigation/router.web.ts new file mode 100644 index 00000000..b683eae0 --- /dev/null +++ b/front/packages/primitives/src/navigation/router.web.ts @@ -0,0 +1,18 @@ +import { navigate } from "vike/client/router"; +import { type UrlObject, format } from "node:url"; + +export const useRouter = () => { + return { + push: (route: string | UrlObject) => { + if (typeof route === "object") route = format(route); + navigate(route); + }, + replace: (route: string | UrlObject, isNested = true) => { + if (typeof route === "object") route = format(route); + navigate(route, { overwriteLastHistoryEntry: true }); + }, + back: () => { + window.history.back(); + }, + }; +}; diff --git a/front/packages/ui/src/login/login.tsx b/front/packages/ui/src/login/login.tsx index 9a53710c..cfdbea09 100644 --- a/front/packages/ui/src/login/login.tsx +++ b/front/packages/ui/src/login/login.tsx @@ -44,10 +44,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({ const { css } = useYoshiki(); useEffect(() => { - if (!apiUrl && Platform.OS !== "web") - router.replace("/server-url", undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); + if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", false); }, [apiUrl, router]); return ( @@ -73,9 +70,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({ }); setError(error); if (error) return; - router.replace("/", undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); + router.replace("/", false); }} {...css({ m: ts(1), diff --git a/front/packages/ui/src/login/oidc.tsx b/front/packages/ui/src/login/oidc.tsx index 1f06ad5a..44b31e3e 100644 --- a/front/packages/ui/src/login/oidc.tsx +++ b/front/packages/ui/src/login/oidc.tsx @@ -26,12 +26,11 @@ import { oidcLogin, useFetch, } from "@kyoo/models"; -import { Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives"; -import { useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { ImageBackground, View } from "react-native"; -import { useRouter } from "solito/router"; +import { useRouter, Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives"; +import { View, ImageBackground } from "react-native"; import { percent, rem, useYoshiki } from "yoshiki/native"; +import { useTranslation } from "react-i18next"; +import { useEffect, useRef } from "react"; import { ErrorView } from "../errors"; export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => { @@ -105,18 +104,12 @@ export const OidcCallbackPage: QueryPage<{ hasRun.current = true; function onError(error: string) { - router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); + router.replace({ pathname: "/login", query: { error, apiUrl } }, false); } async function run() { const { error: loginError } = await oidcLogin(provider, code, apiUrl); if (loginError) onError(loginError); - else { - router.replace("/", undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); - } + else router.replace("/", false); } if (error) onError(error); diff --git a/front/packages/ui/src/login/register.tsx b/front/packages/ui/src/login/register.tsx index 23d2c772..efca0d1e 100644 --- a/front/packages/ui/src/login/register.tsx +++ b/front/packages/ui/src/login/register.tsx @@ -18,13 +18,11 @@ * along with Kyoo. If not, see . */ -import { type QueryPage, login } from "@kyoo/models"; -import { A, Button, H1, Input, P, ts } from "@kyoo/primitives"; +import { login, type QueryPage } from "@kyoo/models"; +import { Button, P, Input, ts, H1, A, useRouter } from "@kyoo/primitives"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Trans } from "react-i18next"; -import { Platform } from "react-native"; -import { useRouter } from "solito/router"; import { percent, px, useYoshiki } from "yoshiki/native"; import { DefaultLayout } from "../layout"; import { FormPage } from "./form"; @@ -43,10 +41,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => { const { css } = useYoshiki(); useEffect(() => { - if (!apiUrl && Platform.OS !== "web") - router.replace("/server-url", undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); + if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", false); }, [apiUrl, router]); return ( @@ -84,9 +79,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => { const { error } = await login("register", { email, username, password, apiUrl }); setError(error); if (error) return; - router.replace("/", undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, - }); + router.replace("/", false); }} {...css({ m: ts(1), diff --git a/front/packages/ui/src/login/server-url.tsx b/front/packages/ui/src/login/server-url.tsx index a2b6ad33..73625081 100644 --- a/front/packages/ui/src/login/server-url.tsx +++ b/front/packages/ui/src/login/server-url.tsx @@ -25,11 +25,10 @@ import { ServerInfoP, useFetch, } from "@kyoo/models"; -import { Button, H1, HR, Input, Link, P, ts } from "@kyoo/primitives"; +import { useRouter, Button, P, Link, Input, ts, H1, HR } from "@kyoo/primitives"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ImageBackground, Platform, View } from "react-native"; -import { useRouter } from "solito/router"; import { type Theme, percent, useYoshiki } from "yoshiki/native"; import { DefaultLayout } from "../layout"; diff --git a/front/packages/ui/src/player/components/hover.tsx b/front/packages/ui/src/player/components/hover.tsx index db0f8b45..716f0033 100644 --- a/front/packages/ui/src/player/components/hover.tsx +++ b/front/packages/ui/src/player/components/hover.tsx @@ -18,7 +18,6 @@ * along with Kyoo. If not, see . */ -import type { Audio, Chapter, KyooImage, Subtitle } from "@kyoo/models"; import { CircularProgress, ContrastArea, @@ -35,15 +34,16 @@ import { tooltip, ts, useIsTouch, + useRouter, } from "@kyoo/primitives"; -import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { atom } from "jotai"; -import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import type { Chapter, KyooImage, Subtitle, Audio } from "@kyoo/models"; +import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { type ImageStyle, Platform, Pressable, View, type ViewProps } from "react-native"; -import { useRouter } from "solito/router"; +import { useTranslation } from "react-i18next"; import { percent, rem, useYoshiki } from "yoshiki/native"; +import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; +import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; +import { atom } from "jotai"; import { bufferedAtom, durationAtom, diff --git a/front/packages/ui/src/player/index.tsx b/front/packages/ui/src/player/index.tsx index bd496e29..01a1fdad 100644 --- a/front/packages/ui/src/player/index.tsx +++ b/front/packages/ui/src/player/index.tsx @@ -28,12 +28,11 @@ import { WatchInfoP, useFetch, } from "@kyoo/models"; -import { Head } from "@kyoo/primitives"; -import { useSetAtom } from "jotai"; -import { type ComponentProps, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Head, useRouter } from "@kyoo/primitives"; +import { useState, useEffect, type ComponentProps } from "react"; import { Platform, StyleSheet, View } from "react-native"; -import { useRouter } from "solito/router"; +import { useTranslation } from "react-i18next"; +import { useSetAtom } from "jotai"; import { useYoshiki } from "yoshiki/native"; import { episodeDisplayNumber } from "../details/episode"; import { ErrorView } from "../errors"; @@ -159,14 +158,8 @@ export const Player = ({ startTime={startTime} onEnd={() => { if (!data) return; - if (data.type === "movie") - router.replace(`/movie/${data.slug}`, undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true }, - }); - else - router.replace(next ?? `/show/${data.show!.slug}`, undefined, { - experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true }, - }); + if (data.type === "movie") router.replace(`/movie/${data.slug}`, true); + else router.replace(next ?? `/show/${data.show!.slug}`, true); }} {...css(StyleSheet.absoluteFillObject)} /> diff --git a/front/packages/ui/src/player/keyboard.tsx b/front/packages/ui/src/player/keyboard.tsx index 5cd828b9..6fa18483 100644 --- a/front/packages/ui/src/player/keyboard.tsx +++ b/front/packages/ui/src/player/keyboard.tsx @@ -20,9 +20,9 @@ import type { Subtitle } from "@kyoo/models"; import { atom, useSetAtom } from "jotai"; +import { useRouter } from "@kyoo/primitives"; import { useEffect } from "react"; import { Platform } from "react-native"; -import { useRouter } from "solito/router"; import { durationAtom, fullscreenAtom, diff --git a/front/packages/ui/src/player/media-session.tsx b/front/packages/ui/src/player/media-session.tsx index 07f3f588..58b90eb6 100644 --- a/front/packages/ui/src/player/media-session.tsx +++ b/front/packages/ui/src/player/media-session.tsx @@ -19,8 +19,8 @@ */ import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { useRouter } from "@kyoo/primitives"; import { useEffect } from "react"; -import { useRouter } from "solito/router"; import { reducerAtom } from "./keyboard"; import { durationAtom, playAtom, progressAtom } from "./state";