wip: Rewrite router

This commit is contained in:
Zoe Roux 2024-05-10 21:14:22 +02:00
parent d953e9e769
commit cb65325c56
No known key found for this signature in database
16 changed files with 116 additions and 73 deletions

View File

@ -22,7 +22,7 @@ import { SearchPage } from "@kyoo/ui";
import { Stack, useLocalSearchParams } from "expo-router"; import { Stack, useLocalSearchParams } from "expo-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createParam } from "solito"; import { createParam } from "solito";
import { useRouter } from "solito/router"; import { useRouter } from "@kyoo/primitives";
import { useTheme } from "yoshiki/native"; import { useTheme } from "yoshiki/native";
const { useParam } = createParam<{ q?: string }>(); const { useParam } = createParam<{ q?: string }>();

View File

@ -54,8 +54,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/html-elements": "^0.9.1", "@expo/html-elements": "^0.9.1",
"@tanstack/react-query": "^5.17.19", "@tanstack/react-query": "^5.17.19"
"solito": "^4.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",

View File

@ -42,3 +42,4 @@ export * from "./chip";
export * from "./utils"; export * from "./utils";
export * from "./constants"; export * from "./constants";
export * from "./navigation/router";

View File

@ -18,19 +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 type { UrlObject } from "node:url"; import { forwardRef, type ReactNode } from "react";
import { type ReactNode, forwardRef } from "react"; import { Platform, Pressable, type TextProps, type View, type PressableProps } from "react-native";
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 { useTheme, useYoshiki } from "yoshiki/native"; import { useTheme, useYoshiki } from "yoshiki/native";
import type { UrlObject } from "node:url";
import { alpha } from "./themes"; import { alpha } from "./themes";
export const A = ({ export const A = ({
@ -122,9 +113,7 @@ export const Link = ({
{...props} {...props}
onPress={(e?: any) => { onPress={(e?: any) => {
props?.onPress?.(e); props?.onPress?.(e);
if (e?.defaultPrevented) return; linkProps.onPress(e);
if (Platform.OS !== "web" && href?.includes("://")) Linking.openURL(href);
else linkProps.onPress(e);
}} }}
> >
{children} {children}

View File

@ -39,6 +39,7 @@ 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 { Icon, IconButton } from "./icons"; import { Icon, IconButton } from "./icons";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { useRouter } from "./navigation/router";
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";

View File

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

View File

@ -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();
},
};
};

View File

@ -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();
},
};
};

View File

@ -44,10 +44,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
const { css } = useYoshiki(); const { css } = useYoshiki();
useEffect(() => { useEffect(() => {
if (!apiUrl && Platform.OS !== "web") if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", false);
router.replace("/server-url", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
}, [apiUrl, router]); }, [apiUrl, router]);
return ( return (
@ -73,9 +70,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
}); });
setError(error); setError(error);
if (error) return; if (error) return;
router.replace("/", undefined, { router.replace("/", false);
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
}} }}
{...css({ {...css({
m: ts(1), m: ts(1),

View File

@ -26,12 +26,11 @@ import {
oidcLogin, oidcLogin,
useFetch, useFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives"; import { useRouter, Button, HR, Link, P, Skeleton, ts } from "@kyoo/primitives";
import { useEffect, useRef } from "react"; import { View, ImageBackground } from "react-native";
import { useTranslation } from "react-i18next";
import { ImageBackground, View } from "react-native";
import { useRouter } from "solito/router";
import { percent, rem, useYoshiki } from "yoshiki/native"; import { percent, rem, useYoshiki } from "yoshiki/native";
import { useTranslation } from "react-i18next";
import { useEffect, useRef } from "react";
import { ErrorView } from "../errors"; import { ErrorView } from "../errors";
export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => { export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => {
@ -105,18 +104,12 @@ export const OidcCallbackPage: QueryPage<{
hasRun.current = true; hasRun.current = true;
function onError(error: string) { function onError(error: string) {
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, { router.replace({ pathname: "/login", query: { error, apiUrl } }, false);
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
} }
async function run() { async function run() {
const { error: loginError } = await oidcLogin(provider, code, apiUrl); const { error: loginError } = await oidcLogin(provider, code, apiUrl);
if (loginError) onError(loginError); if (loginError) onError(loginError);
else { else router.replace("/", false);
router.replace("/", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
}
} }
if (error) onError(error); if (error) onError(error);

View File

@ -18,13 +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 { type QueryPage, login } from "@kyoo/models"; import { login, type QueryPage } from "@kyoo/models";
import { A, Button, H1, Input, P, ts } from "@kyoo/primitives"; import { Button, P, Input, ts, H1, A, useRouter } from "@kyoo/primitives";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Trans } 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 { percent, px, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { FormPage } from "./form"; import { FormPage } from "./form";
@ -43,10 +41,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
useEffect(() => { useEffect(() => {
if (!apiUrl && Platform.OS !== "web") if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", false);
router.replace("/server-url", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
}, [apiUrl, router]); }, [apiUrl, router]);
return ( return (
@ -84,9 +79,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
const { error } = await login("register", { email, username, password, apiUrl }); const { error } = await login("register", { email, username, password, apiUrl });
setError(error); setError(error);
if (error) return; if (error) return;
router.replace("/", undefined, { router.replace("/", false);
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
});
}} }}
{...css({ {...css({
m: ts(1), m: ts(1),

View File

@ -25,11 +25,10 @@ import {
ServerInfoP, ServerInfoP,
useFetch, useFetch,
} from "@kyoo/models"; } 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 { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ImageBackground, Platform, View } from "react-native"; import { ImageBackground, Platform, View } from "react-native";
import { useRouter } from "solito/router";
import { type Theme, percent, useYoshiki } from "yoshiki/native"; import { type Theme, percent, useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";

View File

@ -18,7 +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 type { Audio, Chapter, KyooImage, Subtitle } from "@kyoo/models";
import { import {
CircularProgress, CircularProgress,
ContrastArea, ContrastArea,
@ -35,15 +34,16 @@ import {
tooltip, tooltip,
ts, ts,
useIsTouch, useIsTouch,
useRouter,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; import type { Chapter, KyooImage, Subtitle, Audio } from "@kyoo/models";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { atom } from "jotai";
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { type ImageStyle, Platform, Pressable, View, type ViewProps } from "react-native"; 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 { 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 { import {
bufferedAtom, bufferedAtom,
durationAtom, durationAtom,

View File

@ -28,12 +28,11 @@ import {
WatchInfoP, WatchInfoP,
useFetch, useFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { Head } from "@kyoo/primitives"; import { Head, useRouter } from "@kyoo/primitives";
import { useSetAtom } from "jotai"; import { useState, useEffect, type ComponentProps } from "react";
import { type ComponentProps, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, StyleSheet, View } from "react-native"; 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 { useYoshiki } from "yoshiki/native";
import { episodeDisplayNumber } from "../details/episode"; import { episodeDisplayNumber } from "../details/episode";
import { ErrorView } from "../errors"; import { ErrorView } from "../errors";
@ -159,14 +158,8 @@ export const Player = ({
startTime={startTime} startTime={startTime}
onEnd={() => { onEnd={() => {
if (!data) return; if (!data) return;
if (data.type === "movie") if (data.type === "movie") router.replace(`/movie/${data.slug}`, true);
router.replace(`/movie/${data.slug}`, undefined, { else router.replace(next ?? `/show/${data.show!.slug}`, true);
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
else
router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}} }}
{...css(StyleSheet.absoluteFillObject)} {...css(StyleSheet.absoluteFillObject)}
/> />

View File

@ -20,9 +20,9 @@
import type { Subtitle } from "@kyoo/models"; import type { Subtitle } from "@kyoo/models";
import { atom, useSetAtom } from "jotai"; import { atom, useSetAtom } from "jotai";
import { useRouter } from "@kyoo/primitives";
import { useEffect } from "react"; import { useEffect } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { useRouter } from "solito/router";
import { import {
durationAtom, durationAtom,
fullscreenAtom, fullscreenAtom,

View File

@ -19,8 +19,8 @@
*/ */
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useRouter } from "@kyoo/primitives";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "solito/router";
import { reducerAtom } from "./keyboard"; import { reducerAtom } from "./keyboard";
import { durationAtom, playAtom, progressAtom } from "./state"; import { durationAtom, playAtom, progressAtom } from "./state";