mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 07:20:33 -04:00
Add a multiaccount menu on mobile
This commit is contained in:
parent
12f685010b
commit
fee59833f2
@ -24,6 +24,7 @@ import { zdate } from "./utils";
|
|||||||
import { queryFn } from "./query";
|
import { queryFn } from "./query";
|
||||||
import { KyooErrors } from "./kyoo-errors";
|
import { KyooErrors } from "./kyoo-errors";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const TokenP = z.object({
|
const TokenP = z.object({
|
||||||
token_type: z.literal("Bearer"),
|
token_type: z.literal("Bearer"),
|
||||||
@ -38,8 +39,21 @@ type Result<A, B> =
|
|||||||
| { ok: true; value: A; error?: undefined }
|
| { ok: true; value: A; error?: undefined }
|
||||||
| { ok: false; value?: undefined; error: B };
|
| { ok: false; value?: undefined; error: B };
|
||||||
|
|
||||||
export type Account = Token & { apiUrl: string; username: string | null };
|
export type Account = Token & { apiUrl: string; username: string };
|
||||||
|
|
||||||
|
export const useAccounts = () => {
|
||||||
|
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||||
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function run() {
|
||||||
|
setAccounts(JSON.parse(await getSecureItem("accounts") ?? "[]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
}, []);
|
||||||
|
return {accounts, selected, setSelected};
|
||||||
|
}
|
||||||
|
|
||||||
const addAccount = async (token: Token, apiUrl: string, username: string | null): Promise<void> => {
|
const addAccount = async (token: Token, apiUrl: string, username: string | null): Promise<void> => {
|
||||||
const accounts: Account[] = JSON.parse(await getSecureItem("accounts") ?? "[]");
|
const accounts: Account[] = JSON.parse(await getSecureItem("accounts") ?? "[]");
|
||||||
|
@ -72,7 +72,8 @@ export const Avatar = forwardRef<
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
borderRadius: 999999,
|
borderRadius: 999999,
|
||||||
p: ts(1),
|
height: size,
|
||||||
|
width: size,
|
||||||
},
|
},
|
||||||
fill && {
|
fill && {
|
||||||
bg: col,
|
bg: col,
|
||||||
@ -92,8 +93,6 @@ export const Avatar = forwardRef<
|
|||||||
<P
|
<P
|
||||||
{...css({
|
{...css({
|
||||||
marginVertical: 0,
|
marginVertical: 0,
|
||||||
height: size,
|
|
||||||
width: size,
|
|
||||||
lineHeight: size,
|
lineHeight: size,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
})}
|
})}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { Portal } from "@gorhom/portal";
|
import { Portal } from "@gorhom/portal";
|
||||||
import { ScrollView } from "moti";
|
import { ScrollView } from "moti";
|
||||||
import { ComponentType, createContext, ReactNode, useContext, useEffect, useState } from "react";
|
import { ComponentType, createContext, ReactElement, ReactNode, useContext, useEffect, useState } from "react";
|
||||||
import { StyleSheet, Pressable } from "react-native";
|
import { StyleSheet, Pressable } from "react-native";
|
||||||
import { percent, px, sm, useYoshiki, xl } from "yoshiki/native";
|
import { percent, px, sm, useYoshiki, xl } from "yoshiki/native";
|
||||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||||
@ -114,6 +114,7 @@ const Menu = <AsProps,>({
|
|||||||
const MenuItem = ({
|
const MenuItem = ({
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
|
left,
|
||||||
onSelect,
|
onSelect,
|
||||||
href,
|
href,
|
||||||
icon,
|
icon,
|
||||||
@ -121,12 +122,15 @@ const MenuItem = ({
|
|||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
left?: ReactElement;
|
||||||
icon?: ComponentType<SvgProps>;
|
icon?: ComponentType<SvgProps>;
|
||||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const setOpen = useContext(MenuContext);
|
const setOpen = useContext(MenuContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const icn = (icon || selected) && <Icon icon={icon ?? Check} color={theme.paragraph} size={24} {...css({ paddingLeft: icon ? ts(2) : 0 })}/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PressableFeedback
|
<PressableFeedback
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -145,8 +149,10 @@ const MenuItem = ({
|
|||||||
props as any,
|
props as any,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(icon || selected) && <Icon icon={icon ?? Check} color={theme.paragraph} size={24} {...css({ paddingLeft: icon ? ts(2) : 0 })}/>}
|
{left && left}
|
||||||
<P {...css({ paddingLeft: ts(2) + +!(icon || selected) * px(24) })}>{label}</P>
|
{!left && icn && icn}
|
||||||
|
<P {...css({ paddingLeft: ts(2) + +!(icon || selected || left) * px(24), flexGrow: 1 })}>{label}</P>
|
||||||
|
{left && icn && icn}
|
||||||
</PressableFeedback>
|
</PressableFeedback>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import { ComponentProps, ComponentType, forwardRef, ReactNode } from "react";
|
import { ComponentProps, ComponentType, forwardRef, ReactElement, ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PressableProps } from "react-native";
|
import { PressableProps } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/web";
|
import { useYoshiki } from "yoshiki/web";
|
||||||
@ -119,6 +119,7 @@ const Item = forwardRef<
|
|||||||
const MenuItem = ({
|
const MenuItem = ({
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
|
left,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
href,
|
href,
|
||||||
@ -126,6 +127,7 @@ const MenuItem = ({
|
|||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
icon?: ComponentType<SvgProps>;
|
icon?: ComponentType<SvgProps>;
|
||||||
|
left?: ReactElement;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||||
const { css: nCss } = useNativeYoshiki();
|
const { css: nCss } = useNativeYoshiki();
|
||||||
@ -152,6 +154,7 @@ const MenuItem = ({
|
|||||||
props as any,
|
props as any,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{left && left}
|
||||||
{(icon || selected) && (
|
{(icon || selected) && (
|
||||||
<Icon
|
<Icon
|
||||||
icon={icon ?? Dot}
|
icon={icon ?? Dot}
|
||||||
@ -160,7 +163,7 @@ const MenuItem = ({
|
|||||||
{...nCss({ paddingRight: ts(1) })}
|
{...nCss({ paddingRight: ts(1) })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{<P {...nCss(!selected && { paddingLeft: ts(1 + (icon ? 2 : 1)) })}>{label}</P>}
|
{<P {...nCss(!(icon || selected || left) && { paddingLeft: ts(1) })}>{label}</P>}
|
||||||
</Item>
|
</Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
Page,
|
Page,
|
||||||
Paged,
|
Paged,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
|
useAccounts,
|
||||||
User,
|
User,
|
||||||
UserP,
|
UserP,
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
@ -41,6 +42,7 @@ import {
|
|||||||
ts,
|
ts,
|
||||||
Menu,
|
Menu,
|
||||||
PressableFeedback,
|
PressableFeedback,
|
||||||
|
HR,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { Platform, TextInput, View, ViewProps } from "react-native";
|
import { Platform, TextInput, View, ViewProps } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -49,6 +51,8 @@ import { useRouter } from "solito/router";
|
|||||||
import { px, rem, Stylable, useYoshiki } from "yoshiki/native";
|
import { px, rem, Stylable, useYoshiki } from "yoshiki/native";
|
||||||
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
||||||
import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
|
import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
|
||||||
|
import Login from "@material-symbols/svg-400/rounded/login.svg";
|
||||||
|
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||||
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
||||||
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
||||||
import { Fetch, FetchNE } from "../fetch";
|
import { Fetch, FetchNE } from "../fetch";
|
||||||
@ -102,10 +106,17 @@ export const MeQuery: QueryIdentifier<User> = {
|
|||||||
parser: UserP,
|
parser: UserP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDisplayUrl = (url: string) => {
|
||||||
|
url = url.replace(/\/api$/, "");
|
||||||
|
url = url.replace(/https?:\/\//, "");
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
export const NavbarProfile = () => {
|
export const NavbarProfile = () => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { accounts, selected, setSelected } = useAccounts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FetchNE query={MeQuery}>
|
<FetchNE query={MeQuery}>
|
||||||
@ -120,13 +131,24 @@ export const NavbarProfile = () => {
|
|||||||
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
||||||
{...tooltip(username ?? t("navbar.login"))}
|
{...tooltip(username ?? t("navbar.login"))}
|
||||||
>
|
>
|
||||||
|
{accounts.map((x, i) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={x.refresh_token}
|
||||||
|
label={`${x.username} - ${getDisplayUrl(x.apiUrl)}`}
|
||||||
|
left={<Avatar placeholder={x.username} />}
|
||||||
|
selected={selected === i}
|
||||||
|
onSelect={() => setSelected(i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{accounts.length > 0 && <HR />}
|
||||||
{isGuest ? (
|
{isGuest ? (
|
||||||
<>
|
<>
|
||||||
<Menu.Item label={t("login.login")} href="/login" />
|
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
|
||||||
<Menu.Item label={t("login.register")} href="/register" />
|
<Menu.Item label={t("login.register")} icon={Register} href="/register" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Menu.Item label={t("login.add-account")} icon={Login} href="/login" />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={t("login.logout")}
|
label={t("login.logout")}
|
||||||
icon={Logout}
|
icon={Logout}
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"login": {
|
"login": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
|
"add-account": "Add account",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"server": "Server Address",
|
"server": "Server Address",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"login": {
|
"login": {
|
||||||
"login": "Connexion",
|
"login": "Connexion",
|
||||||
"register": "Créer un compte",
|
"register": "Créer un compte",
|
||||||
|
"add-account": "Ajouter un compte",
|
||||||
"logout": "Déconnexion",
|
"logout": "Déconnexion",
|
||||||
"server": "Addresse du serveur",
|
"server": "Addresse du serveur",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user