mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-04-02 23:31:56 -04:00
Sent native users to server url screen
This commit is contained in:
parent
92cdae82d7
commit
1e17f75aaf
@ -246,6 +246,7 @@
|
||||
"try-again": "Try again",
|
||||
"re-login": "Re login",
|
||||
"offline": "You are not connected to internet. Try again later.",
|
||||
"invalid-server": "Could not reach kyoo's server.",
|
||||
"unauthorized": "You are missing the permissions {{permission}} to access this page.",
|
||||
"needVerification": "Your account needs to be verified by your server administrator before you can use it.",
|
||||
"needAccount": "This page can't be accessed in guest mode. You need to create an account or login.",
|
||||
|
||||
@ -8,10 +8,8 @@ import {
|
||||
Text,
|
||||
type TextProps,
|
||||
} from "react-native";
|
||||
import { useTheme } from "yoshiki/native";
|
||||
import { cn } from "~/utils";
|
||||
import { alpha } from "./theme";
|
||||
import { useResolveClassNames } from "uniwind";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export function useLinkTo({
|
||||
href,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "expo-router";
|
||||
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { z } from "zod/v4";
|
||||
import { Account, User } from "~/models";
|
||||
import { RetryableError } from "~/models/retryable-error";
|
||||
@ -11,10 +13,11 @@ import { useStoreValue } from "./settings";
|
||||
export const defaultApiUrl = "";
|
||||
|
||||
export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const accounts = useStoreValue("accounts", z.array(Account)) ?? [];
|
||||
|
||||
const ret = useMemo(() => {
|
||||
const acc = accounts.find((x) => x.selected);
|
||||
const acc = accounts.find((x) => x.selected) ?? accounts[0];
|
||||
return {
|
||||
apiUrl: acc?.apiUrl ?? defaultApiUrl,
|
||||
authToken: acc?.token ?? null,
|
||||
@ -27,6 +30,20 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
};
|
||||
}, [accounts]);
|
||||
|
||||
if (Platform.OS !== "web") {
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: static
|
||||
const router = useRouter();
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: static
|
||||
useEffect(() => {
|
||||
if (!ret.apiUrl) {
|
||||
setTimeout(() => {
|
||||
console.log("go to login");
|
||||
router.replace("/login");
|
||||
}, 0);
|
||||
}
|
||||
}, [ret.apiUrl, router]);
|
||||
}
|
||||
|
||||
// update user's data from kyoo on startup, it could have changed.
|
||||
const {
|
||||
isSuccess: userIsSuccess,
|
||||
@ -62,7 +79,6 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
updateAccount(nUser.id, nUser);
|
||||
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const selectedId = ret.selectedAccount?.id;
|
||||
useEffect(() => {
|
||||
selectedId;
|
||||
|
||||
@ -83,11 +83,11 @@ export const Providers = ({ children }: { children: ReactNode }) => {
|
||||
<QueryProvider>
|
||||
<ThemeProvider>
|
||||
<RnTheme>
|
||||
<AccountProvider>
|
||||
<TranslationsProvider>
|
||||
<TranslationsProvider>
|
||||
<AccountProvider>
|
||||
<PortalProvider>{children}</PortalProvider>
|
||||
</TranslationsProvider>
|
||||
</AccountProvider>
|
||||
</AccountProvider>
|
||||
</TranslationsProvider>
|
||||
</RnTheme>
|
||||
</ThemeProvider>
|
||||
</QueryProvider>
|
||||
|
||||
@ -59,10 +59,9 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
||||
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
||||
throw { message: "Aborted", status: "aborted" } as KyooError;
|
||||
console.log("Fetch error", e, context.url);
|
||||
throw {
|
||||
message: "Could not reach Kyoo's server.",
|
||||
status: "aborted",
|
||||
} as KyooError;
|
||||
throw new RetryableError({
|
||||
key: "offline",
|
||||
});
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
throw { message: "Resource not found.", status: 404 } as KyooError;
|
||||
@ -144,6 +143,7 @@ export type QueryIdentifier<T = unknown> = {
|
||||
enabled?: boolean;
|
||||
options?: Partial<Parameters<typeof queryFn>[0]> & {
|
||||
apiUrl?: string;
|
||||
returnError?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@ -189,14 +189,16 @@ export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
enabled: query.enabled,
|
||||
});
|
||||
|
||||
if (ret.isPaused) throw new RetryableError({ key: "offline" });
|
||||
if (ret.error && (ret.error.status === 401 || ret.error.status === 403)) {
|
||||
throw new RetryableError({
|
||||
key: !selectedAccount ? "needAccount" : "unauthorized",
|
||||
inner: ret.error,
|
||||
});
|
||||
if (query.options?.returnError !== true) {
|
||||
if (ret.isPaused) throw new RetryableError({ key: "offline" });
|
||||
if (ret.error && (ret.error.status === 401 || ret.error.status === 403)) {
|
||||
throw new RetryableError({
|
||||
key: !selectedAccount ? "needAccount" : "unauthorized",
|
||||
inner: ret.error,
|
||||
});
|
||||
}
|
||||
if (ret.error) throw ret.error;
|
||||
}
|
||||
if (ret.error) throw ret.error;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ImageBackground, ScrollView, View } from "react-native";
|
||||
import Svg, { Path, type SvgProps } from "react-native-svg";
|
||||
import { min, px, type Stylable, useYoshiki, vh } from "yoshiki/native";
|
||||
import { ts } from "~/primitives";
|
||||
import {
|
||||
ImageBackground,
|
||||
ScrollView,
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import { Path } from "react-native-svg";
|
||||
import { Svg } from "~/primitives";
|
||||
import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
const SvgBlob = (props: SvgProps) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
const SvgBlob = ({ className, ...props }: ViewProps) => {
|
||||
return (
|
||||
<View {...css({ width: min(vh(90), px(1200)), aspectRatio: 5 / 6 }, props)}>
|
||||
<Svg width="100%" height="100%" viewBox="0 0 500 600">
|
||||
<Path
|
||||
d="M459.7 0c-20.2 43.3-40.3 86.6-51.7 132.6-11.3 45.9-13.9 94.6-36.1 137.6-22.2 43-64.1 80.3-111.5 88.2s-100.2-13.7-144.5-1.8C71.6 368.6 35.8 414.2 0 459.7V0h459.7z"
|
||||
fill={theme.background}
|
||||
/>
|
||||
<View className={cn("aspect-5/6 w-[90vh] max-w-5xl", className)} {...props}>
|
||||
<Svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 500 600"
|
||||
className="fill-background"
|
||||
>
|
||||
<Path d="M459.7 0c-20.2 43.3-40.3 86.6-51.7 132.6-11.3 45.9-13.9 94.6-36.1 137.6-22.2 43-64.1 80.3-111.5 88.2s-100.2-13.7-144.5-1.8C71.6 368.6 35.8 414.2 0 459.7V0h459.7z" />
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
@ -23,37 +28,26 @@ const SvgBlob = (props: SvgProps) => {
|
||||
export const FormPage = ({
|
||||
children,
|
||||
apiUrl,
|
||||
className,
|
||||
...props
|
||||
}: { children: ReactNode; apiUrl?: string } & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
}: {
|
||||
children: ReactNode;
|
||||
apiUrl?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ImageBackground
|
||||
source={{ uri: `${apiUrl ?? defaultApiUrl}/api/shows/random/thumbnail` }}
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
backgroundColor: (theme) => theme.dark.background,
|
||||
})}
|
||||
className="flex-1 flex-row bg-dark"
|
||||
>
|
||||
<SvgBlob {...css({ position: "absolute", top: 0, left: 0 })} />
|
||||
<ScrollView
|
||||
{...css({
|
||||
paddingRight: ts(3),
|
||||
})}
|
||||
>
|
||||
<SvgBlob className="absolute top-0 left-0" />
|
||||
<ScrollView className="pr-6">
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
maxWidth: px(600),
|
||||
backgroundColor: (theme) => theme.background,
|
||||
borderBottomRightRadius: ts(25),
|
||||
paddingBottom: ts(5),
|
||||
paddingLeft: ts(3),
|
||||
},
|
||||
props,
|
||||
className={cn(
|
||||
"max-w-xl rounded-[25rem] bg-background py-10 pl-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
@ -2,8 +2,7 @@ import { useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { A, Button, H1, Input, P, ts } from "~/primitives";
|
||||
import { A, Button, H1, Input, P } from "~/primitives";
|
||||
import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { FormPage } from "./form";
|
||||
@ -18,31 +17,25 @@ export const LoginPage = () => {
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
const router = useRouter();
|
||||
|
||||
if (Platform.OS !== "web" && !apiUrl) return <ServerUrlPage />;
|
||||
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<H1>{t("login.login")}</H1>
|
||||
{/* <OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} /> */}
|
||||
{/* {data?.passwordLoginEnabled && ( */}
|
||||
{/* <> */}
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.username")}</P>
|
||||
<H1 className="pb-4">{t("login.login")}</H1>
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
variant="big"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.password")}</P>
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
{error && <P className="text-red-500 dark:text-red-500">{error}</P>}
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
onPress={async () => {
|
||||
@ -55,16 +48,8 @@ export const LoginPage = () => {
|
||||
if (error) return;
|
||||
router.replace("/");
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
{/* </> */}
|
||||
{/* )} */}
|
||||
<P>
|
||||
<Trans i18nKey="login.or-register">
|
||||
Don’t have an account?
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import VisibilityOff from "@material-symbols/svg-400/rounded/visibility_off-fill.svg";
|
||||
import Visibility from "@material-symbols/svg-400/rounded/visibility-fill.svg";
|
||||
import { type ComponentProps, useState } from "react";
|
||||
import { px, useYoshiki } from "yoshiki/native";
|
||||
import { IconButton, Input } from "~/primitives";
|
||||
|
||||
export const PasswordInput = (props: ComponentProps<typeof Input>) => {
|
||||
const { css } = useYoshiki();
|
||||
const [show, setVisibility] = useState(false);
|
||||
|
||||
return (
|
||||
@ -14,9 +12,7 @@ export const PasswordInput = (props: ComponentProps<typeof Input>) => {
|
||||
right={
|
||||
<IconButton
|
||||
icon={show ? VisibilityOff : Visibility}
|
||||
size={19}
|
||||
onPress={() => setVisibility(!show)}
|
||||
{...css({ width: px(19), height: px(19), m: 0, p: 0 })}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
|
||||
@ -2,8 +2,7 @@ import { useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { A, Button, H1, Input, P, ts } from "~/primitives";
|
||||
import { A, Button, H1, Input, P } from "~/primitives";
|
||||
import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { FormPage } from "./form";
|
||||
@ -21,50 +20,39 @@ export const RegisterPage = () => {
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
if (Platform.OS !== "web" && !apiUrl) return <ServerUrlPage />;
|
||||
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<H1>{t("login.register")}</H1>
|
||||
{/* <OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} /> */}
|
||||
{/* {data?.registrationEnabled && ( */}
|
||||
{/* <> */}
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.username")}</P>
|
||||
<H1 className="pb-4">{t("login.register")}</H1>
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
variant="big"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
/>
|
||||
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.email")}</P>
|
||||
<Input
|
||||
autoComplete="email"
|
||||
variant="big"
|
||||
onChangeText={(value) => setEmail(value)}
|
||||
/>
|
||||
<P className="pt-2 pl-2">{t("login.email")}</P>
|
||||
<Input autoComplete="email" onChangeText={(value) => setEmail(value)} />
|
||||
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.password")}</P>
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.confirm")}</P>
|
||||
<P className="pt-2 pl-2">{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
|
||||
{password !== confirm && (
|
||||
<P {...css({ color: (theme) => theme.colors.red })}>
|
||||
<P className="text-red-500 dark:text-red-500">
|
||||
{t("login.password-no-match")}
|
||||
</P>
|
||||
)}
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
{error && <P className="text-red-500 dark:text-red-500">{error}</P>}
|
||||
<Button
|
||||
text={t("login.register")}
|
||||
disabled={password !== confirm}
|
||||
@ -79,16 +67,8 @@ export const RegisterPage = () => {
|
||||
if (error) return;
|
||||
router.replace("/");
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
{/* </> */}
|
||||
{/* )} */}
|
||||
<P>
|
||||
<Trans i18nKey="login.or-login">
|
||||
Have an account already?
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { type Theme, useYoshiki } from "yoshiki/native";
|
||||
import { Button, H1, Input, Link, P, ts } from "~/primitives";
|
||||
import { Button, H1, Input, Link, P } from "~/primitives";
|
||||
import { type QueryIdentifier, useFetch } from "~/query";
|
||||
|
||||
export const cleanApiUrl = (apiUrl: string) => {
|
||||
@ -16,73 +15,44 @@ export const ServerUrlPage = () => {
|
||||
const apiUrl = cleanApiUrl(_apiUrl);
|
||||
const { data, error } = useFetch({
|
||||
...ServerUrlPage.query,
|
||||
options: { apiUrl, authToken: null },
|
||||
options: { apiUrl, authToken: null, returnError: true },
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
marginX: ts(3),
|
||||
justifyContent: "space-between",
|
||||
flexGrow: 1,
|
||||
})}
|
||||
>
|
||||
<View className="m-6 flex-1 justify-between">
|
||||
<H1>{t("login.server")}</H1>
|
||||
<View {...css({ justifyContent: "center" })}>
|
||||
<View className="justify-center">
|
||||
<Input
|
||||
variant="big"
|
||||
onChangeText={setApiUrl}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{!data && (
|
||||
<P
|
||||
{...css({
|
||||
color: (theme: Theme) => theme.colors.red,
|
||||
alignSelf: "center",
|
||||
})}
|
||||
>
|
||||
{error?.message ?? t("misc.loading")}
|
||||
<P className="self-center text-red-500 dark:text-red-500">
|
||||
{error
|
||||
? error.message === "offline"
|
||||
? t("errors.invalid-server")
|
||||
: error.message
|
||||
: t("misc.loading")}
|
||||
</P>
|
||||
)}
|
||||
</View>
|
||||
<View {...css({ marginTop: ts(5) })}>
|
||||
{/* <View {...css({ flexDirection: "row", width: percent(100), alignItems: "center" })}> */}
|
||||
{/* {data && */}
|
||||
{/* Object.values(data.oidc).map((x) => ( */}
|
||||
{/* <Link */}
|
||||
{/* key={x.displayName} */}
|
||||
{/* href={{ pathname: x.link, query: { apiUrl } }} */}
|
||||
{/* {...css({ justifyContent: "center" })} */}
|
||||
{/* > */}
|
||||
{/* {x.logoUrl != null ? ( */}
|
||||
{/* <ImageBackground */}
|
||||
{/* source={{ uri: x.logoUrl }} */}
|
||||
{/* {...css({ width: ts(3), height: ts(3), margin: ts(1) })} */}
|
||||
{/* /> */}
|
||||
{/* ) : ( */}
|
||||
{/* t("login.via", { provider: x.displayName }) */}
|
||||
{/* )} */}
|
||||
{/* </Link> */}
|
||||
{/* ))} */}
|
||||
{/* </View> */}
|
||||
{/* <HR /> */}
|
||||
<View {...css({ flexDirection: "row", gap: ts(2) })}>
|
||||
<View className="mb-2">
|
||||
<View className="flex-row gap-4">
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
as={Link}
|
||||
href={`/login?apiUrl=${apiUrl}`}
|
||||
disabled={data == null}
|
||||
{...css({ flexGrow: 1, flexShrink: 1 })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
text={t("login.register")}
|
||||
as={Link}
|
||||
href={`/register?apiUrl=${apiUrl}`}
|
||||
disabled={data == null}
|
||||
{...css({ flexGrow: 1, flexShrink: 1 })}
|
||||
className="flex-1"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user