Rework router

This commit is contained in:
Zoe Roux 2024-05-11 11:27:53 +02:00
parent cb65325c56
commit d2a6ffc1c7
No known key found for this signature in database
19 changed files with 139 additions and 131 deletions

View File

@ -21,12 +21,9 @@
import { SearchPage } from "@kyoo/ui"; 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 { useRouter, useParam } from "@kyoo/primitives";
import { useRouter } from "@kyoo/primitives";
import { useTheme } from "yoshiki/native"; import { useTheme } from "yoshiki/native";
const { useParam } = createParam<{ q?: string }>();
const Search = () => { const Search = () => {
const theme = useTheme(); const theme = useTheme();
const { back } = useRouter(); const { back } = useRouter();
@ -52,7 +49,7 @@ const Search = () => {
}, },
}} }}
/> />
<SearchPage {...routeParams} /> <SearchPage q={query} />
</> </>
); );
}; };

View File

@ -22,6 +22,7 @@ import "~/polyfill";
// typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo; // typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
// return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />; // return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />;
// }; // };
const GlobalCssTheme = () => { const GlobalCssTheme = () => {
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -76,35 +77,36 @@ const GlobalCssTheme = () => {
}; };
export default function Layout({ children }: { children: ReactNode }) { export default function Layout({ children }: { children: ReactNode }) {
// TODO: theme ssr // TODO: theme ssr
const userTheme = useUserTheme(undefined); // const userTheme = useUserTheme(undefined);
useMobileHover(); useMobileHover();
// TODO: ssr account/error // TODO: ssr account/error
return ( return (
<> <>
<AccountProvider ssrAccount={undefined} ssrError={undefined}> {/* <AccountProvider ssrAccount={undefined} ssrError={undefined}> */}
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}> {/* <ThemeSelector theme={userTheme} font={{ normal: "inherit" }}> */}
<PortalProvider> {/* <PortalProvider> */}
<SnackbarProvider> {/* <SnackbarProvider> */}
<GlobalCssTheme /> <GlobalCssTheme />
{children} {children}
{/* <ConnectionErrorVerifier skipErrors={(Component as QueryPage).isPublic}> {/* {/* <ConnectionErrorVerifier skipErrors={(Component as QueryPage).isPublic}> */}
<WithLayout {/* <WithLayout */}
Component={children} {/* Component={children} */}
randomItems={ {/* randomItems={ */}
randomItems[Component.displayName!] ?? {/* randomItems[Component.displayName!] ?? */}
arrayShuffle((Component as QueryPage).randomItems ?? []) {/* arrayShuffle((Component as QueryPage).randomItems ?? []) */}
} {/* } */}
{...props} {/* {...props} */}
/> {/* /> */}
</ConnectionErrorVerifier> */} {/* </ConnectionErrorVerifier> */}
<Tooltip id="tooltip" positionStrategy={"fixed"} /> {/* <Tooltip id="tooltip" positionStrategy={"fixed"} /> */}
</SnackbarProvider> {/* </SnackbarProvider> */}
</PortalProvider> {/* </PortalProvider> */}
</ThemeSelector> {/* </ThemeSelector> */}
</AccountProvider> {/* </AccountProvider> */}
<ReactQueryDevtools initialIsOpen={false} /> {/* <ReactQueryDevtools initialIsOpen={false} /> */}
</> </>
); );
} }

View File

@ -1,11 +1,10 @@
import type { Config } from "vike/types"; import type { Config } from "vike/types";
import logoUrl from "../../public/icon.svg";
import vikeReact from "vike-react/config"; import vikeReact from "vike-react/config";
import vikeReactQuery from "vike-react-query/config"; import vikeReactQuery from "vike-react-query/config";
export default { export default {
ssr: true, ssr: true,
title: "Kyoo", title: "Kyoo",
favicon: logoUrl,
extends: [vikeReact, vikeReactQuery], extends: [vikeReact, vikeReactQuery],
} satisfies Config; } satisfies Config;

View File

@ -1,3 +1,9 @@
import { HomePage } from "@kyoo/ui"; // import { HomePage } from "@kyoo/ui";
export default HomePage; // export default HomePage;
export default function Test() {
return <div>
<p>tosej</p>
</div>
}

View File

@ -13,5 +13,15 @@ export default {
"~": path.resolve(__dirname, "./src"), "~": path.resolve(__dirname, "./src"),
}, },
}, },
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
},
optimizeDeps: {
esbuildOptions: {
mainFields: ["module", "main"],
},
},
plugins: [react(), vike(), reactNativeWeb()], plugins: [react(), vike(), reactNativeWeb()],
} satisfies UserConfig; } satisfies UserConfig;

View File

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

View File

@ -23,6 +23,8 @@ import { Platform, Pressable, type TextProps, type View, type PressableProps } f
import { useTheme, useYoshiki } from "yoshiki/native"; import { useTheme, useYoshiki } from "yoshiki/native";
import type { UrlObject } from "node:url"; import type { UrlObject } from "node:url";
import { alpha } from "./themes"; import { alpha } from "./themes";
import { P } from "./text";
import { useLink } from "./navigation/link";
export const A = ({ export const A = ({
href, href,
@ -32,43 +34,16 @@ export const A = ({
...props ...props
}: TextProps & { }: TextProps & {
href?: string | UrlObject | null; href?: string | UrlObject | null;
target?: string; target?: "_blank";
replace?: boolean; replace?: boolean;
children: ReactNode; children: ReactNode;
}) => { }) => {
const { css, theme } = useYoshiki(); const link = useLink(href ?? "#", { target, replace });
return ( return (
<TextLink <P {...link} {...props}>
href={href ?? ""}
target={target}
replace={replace as any}
experimental={
replace
? {
nativeBehavior: "stack-replace",
isNestedNavigator: true,
}
: undefined
}
textProps={css(
[
{
fontFamily: theme.font.normal,
color: theme.link,
},
{
userSelect: "text",
} as any,
],
{
hrefAttrs: { target },
...props,
},
)}
>
{children} {children}
</TextLink> </P>
); );
}; };
@ -93,20 +68,17 @@ export const PressableFeedback = forwardRef<View, PressableProps>(function Feedb
}); });
export const Link = ({ export const Link = ({
href: link, href,
replace, replace,
target, target,
children, children,
...props ...props
}: { href?: string | UrlObject | null; target?: string; replace?: boolean } & PressableProps) => { }: { href?: string | UrlObject | null; target?: "_blank"; replace?: boolean } & PressableProps) => {
const href = link && typeof link === "object" ? parseNextPath(link) : link; const linkProps = useLink(href ?? "#", {
const linkProps = useLink({ target,
href: href ?? "#",
replace, replace,
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true }, isNested: true,
}); });
// @ts-ignore Missing hrefAttrs type definition.
linkProps.hrefAttrs = { ...linkProps.hrefAttrs, target };
return ( return (
<PressableFeedback <PressableFeedback
{...linkProps} {...linkProps}

View File

@ -15,8 +15,9 @@ export const useLink = (
href, href,
onPress: (e) => { onPress: (e) => {
if (e?.defaultPrevented) return; if (e?.defaultPrevented) return;
if (Platform.OS !== "web" && href.includes("://")) { const abs = href.includes("://");
Linking.openURL(href); if (Platform.OS !== "web" && (abs || opts.target)) {
Linking.openURL(abs ? href : `https://${href}`);
return; return;
} }
@ -31,7 +32,9 @@ export const useLink = (
we.shiftKey || we.shiftKey ||
// ignore everything but left clicks // ignore everything but left clicks
we.button !== null || we.button !== null ||
we.button !== 0 we.button !== 0 ||
// let the browser handle target blank
opts.target
) )
return; return;
} }

View File

@ -0,0 +1,23 @@
import { useCallback } from "react";
import { usePageContext } from "vike-react/usePageContext";
import { useRouter } from "./router";
export const useParam = (name: string) => {
const { urlParsed } = usePageContext();
const router = useRouter();
const val = urlParsed.search[name];
const setState = useCallback(
(newVal: string | null) => {
if (newVal) urlParsed.search[name] = newVal;
else delete urlParsed.search[name];
router.replace({
...urlParsed,
search: Object.entries(urlParsed.search).join("&"),
});
},
[router],
);
return [val, setState] as const;
};

View File

@ -1,18 +1,23 @@
import { navigate } from "vike/client/router"; import { navigate } from "vike/client/router";
import { type UrlObject, format } from "node:url"; import { type UrlObject, format } from "node:url";
import { useMemo } from "react";
export const useRouter = () => { export const useRouter = () => {
return { const ret = useMemo(
() => ({
push: (route: string | UrlObject) => { push: (route: string | UrlObject) => {
if (typeof route === "object") route = format(route); if (typeof route === "object") route = format(route);
navigate(route); navigate(route);
}, },
replace: (route: string | UrlObject, opts: {isNested?: boolean} = {}) => { replace: (route: string | UrlObject, opts?: { isNested?: boolean }) => {
if (typeof route === "object") route = format(route); if (typeof route === "object") route = format(route);
navigate(route, { overwriteLastHistoryEntry: opts.isNested }); navigate(route, { overwriteLastHistoryEntry: true });
}, },
back: () => { back: () => {
window.history.back(); window.history.back();
}, },
}; }),
[],
);
return ret;
}; };

View File

@ -1,18 +1,23 @@
import { navigate } from "vike/client/router"; import { navigate } from "vike/client/router";
import { type UrlObject, format } from "node:url"; import { type UrlObject, format } from "node:url";
import { useMemo } from "react";
export const useRouter = () => { export const useRouter = () => {
return { const ret = useMemo(
() => ({
push: (route: string | UrlObject) => { push: (route: string | UrlObject) => {
if (typeof route === "object") route = format(route); if (typeof route === "object") route = format(route);
navigate(route); navigate(route);
}, },
replace: (route: string | UrlObject, isNested = true) => { replace: (route: string | UrlObject, opts?: { isNested?: boolean }) => {
if (typeof route === "object") route = format(route); if (typeof route === "object") route = format(route);
navigate(route, { overwriteLastHistoryEntry: true }); navigate(route, { overwriteLastHistoryEntry: true });
}, },
back: () => { back: () => {
window.history.back(); window.history.back();
}, },
}; }),
[],
);
return ret;
}; };

View File

@ -25,18 +25,16 @@ import {
type QueryPage, type QueryPage,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { useParam } from "@kyoo/primitives";
import { type ComponentProps, useState } from "react"; import { type ComponentProps, useState } from "react";
import { createParam } from "solito"; import { DefaultLayout } from "../layout";
import type { WithLoading } from "../fetch"; import type { 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 { BrowseSettings } from "./header"; import { BrowseSettings } from "./header";
import { ItemList } from "./list"; import { ItemList } from "./list";
import { Layout, SortBy, SortOrd } from "./types"; import { Layout, SortBy, SortOrd } from "./types";
const { useParam } = createParam<{ sortBy?: string }>();
export const itemMap = ( export const itemMap = (
item: WithLoading<LibraryItem>, item: WithLoading<LibraryItem>,
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => { ): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {

View File

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

View File

@ -104,12 +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 } }, false); router.replace({ pathname: "/login", query: { error, apiUrl } }, { isNested: 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 router.replace("/", false); else router.replace("/", { isNested: false });
} }
if (error) onError(error); if (error) onError(error);

View File

@ -41,7 +41,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
useEffect(() => { useEffect(() => {
if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", false); if (!apiUrl && Platform.OS !== "web") router.replace("/server-url", { isNested: false });
}, [apiUrl, router]); }, [apiUrl, router]);
return ( return (
@ -79,7 +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("/", false); router.replace("/", { isNested: false });
}} }}
{...css({ {...css({
m: ts(1), m: ts(1),

View File

@ -29,6 +29,7 @@ import {
Link, Link,
Menu, Menu,
PressableFeedback, PressableFeedback,
useRouter,
tooltip, tooltip,
ts, ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
@ -40,11 +41,10 @@ import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
import Settings from "@material-symbols/svg-400/rounded/settings.svg"; import Settings from "@material-symbols/svg-400/rounded/settings.svg";
import { forwardRef, useEffect, useRef, useState } from "react"; import { forwardRef, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, type TextInput, View, type ViewProps } from "react-native";
import { useRouter } from "solito/router";
import { type Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, useYoshiki } from "yoshiki/native";
import { AdminPage } from "../admin"; import { AdminPage } from "../admin";
import { KyooLongLogo } from "./icon"; import { KyooLongLogo } from "./icon";
import { Platform, TextInput, View, ViewProps } from "react-native";
export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }) => { export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -66,7 +66,7 @@ const SearchBar = forwardRef<TextInput, Stylable>(function SearchBar(props, ref)
useEffect(() => { useEffect(() => {
if (Platform.OS !== "web" || !hasChanged.current) return; if (Platform.OS !== "web" || !hasChanged.current) return;
const action = window.location.pathname.startsWith("/search") ? replace : push; const action = window.location.pathname.startsWith("/search") ? replace : push;
if (query) action(`/search?q=${encodeURI(query)}`, undefined, { shallow: true }); if (query) action(`/search?q=${encodeURI(query)}`);
else back(); else back();
}, [query, push, replace, back]); }, [query, push, replace, back]);

View File

@ -158,8 +158,8 @@ export const Player = ({
startTime={startTime} startTime={startTime}
onEnd={() => { onEnd={() => {
if (!data) return; if (!data) return;
if (data.type === "movie") router.replace(`/movie/${data.slug}`, true); if (data.type === "movie") router.replace(`/movie/${data.slug}`, { isNested: true });
else router.replace(next ?? `/show/${data.show!.slug}`, true); else router.replace(next ?? `/show/${data.show!.slug}`, { isNested: true });
}} }}
{...css(StyleSheet.absoluteFillObject)} {...css(StyleSheet.absoluteFillObject)}
/> />

View File

@ -19,19 +19,16 @@
*/ */
import { type LibraryItem, LibraryItemP, type QueryIdentifier, type QueryPage } from "@kyoo/models"; import { type LibraryItem, LibraryItemP, type QueryIdentifier, type QueryPage } from "@kyoo/models";
import { usePageStyle } from "@kyoo/primitives";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createParam } from "solito"; import { DefaultLayout } from "../layout";
import { InfiniteFetch } from "../fetch-infinite";
import { itemMap } from "../browse"; import { itemMap } from "../browse";
import { ItemGrid } from "../browse/grid"; import { ItemGrid } from "../browse/grid";
import { BrowseSettings } from "../browse/header"; import { BrowseSettings } from "../browse/header";
import { ItemList } from "../browse/list"; import { ItemList } from "../browse/list";
import { useParam, usePageStyle } from "@kyoo/primitives";
import { Layout, SearchSort, SortOrd } from "../browse/types"; import { Layout, SearchSort, SortOrd } from "../browse/types";
import { InfiniteFetch } from "../fetch-infinite";
import { DefaultLayout } from "../layout";
const { useParam } = createParam<{ sortBy?: string }>();
const query = ( const query = (
query?: string, query?: string,

View File

@ -3144,7 +3144,6 @@ __metadata:
react-native-blurhash: ^1.1.11 react-native-blurhash: ^1.1.11
react-native-fast-image: ^8.6.3 react-native-fast-image: ^8.6.3
react-native-safe-area-context: 4.8.2 react-native-safe-area-context: 4.8.2
solito: ^4.2.0
typescript: ^5.3.3 typescript: ^5.3.3
peerDependencies: peerDependencies:
"@gorhom/portal": "*" "@gorhom/portal": "*"
@ -12079,15 +12078,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"solito@npm:^4.2.0":
version: 4.2.0
resolution: "solito@npm:4.2.0"
dependencies:
typescript: ^5.0.4
checksum: 58ea67fce743cc864e7cb9065d06fa287fd99bab1b0a96c5d9bd1e974740bb4a308620622c9b46975d58ba084127a8388dc7696cc6e171ed6b501843093ab64f
languageName: node
linkType: hard
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2": "source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "source-map-js@npm:1.0.2" resolution: "source-map-js@npm:1.0.2"
@ -12740,7 +12730,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@npm:5.3.3, typescript@npm:^5.0.4, typescript@npm:^5.3.3": "typescript@npm:5.3.3, typescript@npm:^5.3.3":
version: 5.3.3 version: 5.3.3
resolution: "typescript@npm:5.3.3" resolution: "typescript@npm:5.3.3"
bin: bin:
@ -12750,7 +12740,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@patch:typescript@5.3.3#~builtin<compat/typescript>, typescript@patch:typescript@^5.0.4#~builtin<compat/typescript>, typescript@patch:typescript@^5.3.3#~builtin<compat/typescript>": "typescript@patch:typescript@5.3.3#~builtin<compat/typescript>, typescript@patch:typescript@^5.3.3#~builtin<compat/typescript>":
version: 5.3.3 version: 5.3.3
resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=701156" resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=701156"
bin: bin: