mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -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 { KyooErrors } from "./kyoo-errors";
|
||||
import { Platform } from "react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const TokenP = z.object({
|
||||
token_type: z.literal("Bearer"),
|
||||
@ -38,8 +39,21 @@ type Result<A, B> =
|
||||
| { ok: true; value: A; error?: undefined }
|
||||
| { 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 accounts: Account[] = JSON.parse(await getSecureItem("accounts") ?? "[]");
|
||||
|
@ -72,7 +72,8 @@ export const Avatar = forwardRef<
|
||||
[
|
||||
{
|
||||
borderRadius: 999999,
|
||||
p: ts(1),
|
||||
height: size,
|
||||
width: size,
|
||||
},
|
||||
fill && {
|
||||
bg: col,
|
||||
@ -92,8 +93,6 @@ export const Avatar = forwardRef<
|
||||
<P
|
||||
{...css({
|
||||
marginVertical: 0,
|
||||
height: size,
|
||||
width: size,
|
||||
lineHeight: size,
|
||||
textAlign: "center",
|
||||
})}
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import { Portal } from "@gorhom/portal";
|
||||
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 { percent, px, sm, useYoshiki, xl } from "yoshiki/native";
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
@ -114,6 +114,7 @@ const Menu = <AsProps,>({
|
||||
const MenuItem = ({
|
||||
label,
|
||||
selected,
|
||||
left,
|
||||
onSelect,
|
||||
href,
|
||||
icon,
|
||||
@ -121,12 +122,15 @@ const MenuItem = ({
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
left?: ReactElement;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const setOpen = useContext(MenuContext);
|
||||
const router = useRouter();
|
||||
|
||||
const icn = (icon || selected) && <Icon icon={icon ?? Check} color={theme.paragraph} size={24} {...css({ paddingLeft: icon ? ts(2) : 0 })}/>;
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
@ -145,8 +149,10 @@ const MenuItem = ({
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
{(icon || selected) && <Icon icon={icon ?? Check} color={theme.paragraph} size={24} {...css({ paddingLeft: icon ? ts(2) : 0 })}/>}
|
||||
<P {...css({ paddingLeft: ts(2) + +!(icon || selected) * px(24) })}>{label}</P>
|
||||
{left && left}
|
||||
{!left && icn && icn}
|
||||
<P {...css({ paddingLeft: ts(2) + +!(icon || selected || left) * px(24), flexGrow: 1 })}>{label}</P>
|
||||
{left && icn && icn}
|
||||
</PressableFeedback>
|
||||
);
|
||||
};
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
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 { PressableProps } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/web";
|
||||
@ -119,6 +119,7 @@ const Item = forwardRef<
|
||||
const MenuItem = ({
|
||||
label,
|
||||
icon,
|
||||
left,
|
||||
selected,
|
||||
onSelect,
|
||||
href,
|
||||
@ -126,6 +127,7 @@ const MenuItem = ({
|
||||
}: {
|
||||
label: string;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
left?: ReactElement;
|
||||
selected?: boolean;
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||
const { css: nCss } = useNativeYoshiki();
|
||||
@ -152,6 +154,7 @@ const MenuItem = ({
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
{left && left}
|
||||
{(icon || selected) && (
|
||||
<Icon
|
||||
icon={icon ?? Dot}
|
||||
@ -160,7 +163,7 @@ const MenuItem = ({
|
||||
{...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>
|
||||
</>
|
||||
);
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
Page,
|
||||
Paged,
|
||||
QueryIdentifier,
|
||||
useAccounts,
|
||||
User,
|
||||
UserP,
|
||||
} from "@kyoo/models";
|
||||
@ -41,6 +42,7 @@ import {
|
||||
ts,
|
||||
Menu,
|
||||
PressableFeedback,
|
||||
HR,
|
||||
} from "@kyoo/primitives";
|
||||
import { Platform, TextInput, View, ViewProps } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -49,6 +51,8 @@ import { useRouter } from "solito/router";
|
||||
import { px, rem, Stylable, useYoshiki } from "yoshiki/native";
|
||||
import MenuIcon from "@material-symbols/svg-400/rounded/menu-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 Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
||||
import { Fetch, FetchNE } from "../fetch";
|
||||
@ -102,10 +106,17 @@ export const MeQuery: QueryIdentifier<User> = {
|
||||
parser: UserP,
|
||||
};
|
||||
|
||||
const getDisplayUrl = (url: string) => {
|
||||
url = url.replace(/\/api$/, "");
|
||||
url = url.replace(/https?:\/\//, "");
|
||||
return url;
|
||||
};
|
||||
|
||||
export const NavbarProfile = () => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { accounts, selected, setSelected } = useAccounts();
|
||||
|
||||
return (
|
||||
<FetchNE query={MeQuery}>
|
||||
@ -120,13 +131,24 @@ export const NavbarProfile = () => {
|
||||
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
||||
{...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 ? (
|
||||
<>
|
||||
<Menu.Item label={t("login.login")} href="/login" />
|
||||
<Menu.Item label={t("login.register")} href="/register" />
|
||||
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
|
||||
<Menu.Item label={t("login.register")} icon={Register} href="/register" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Menu.Item label={t("login.add-account")} icon={Login} href="/login" />
|
||||
<Menu.Item
|
||||
label={t("login.logout")}
|
||||
icon={Logout}
|
||||
@ -197,9 +219,9 @@ export const NavbarRight = () => {
|
||||
onPress={
|
||||
Platform.OS === "web"
|
||||
? () => {
|
||||
setSearch(true);
|
||||
setTimeout(() => ref.current?.focus(), 0);
|
||||
}
|
||||
setSearch(true);
|
||||
setTimeout(() => ref.current?.focus(), 0);
|
||||
}
|
||||
: () => push("/search")
|
||||
}
|
||||
{...tooltip(t("navbar.search"))}
|
||||
|
@ -60,6 +60,7 @@
|
||||
"login": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"add-account": "Add account",
|
||||
"logout": "Logout",
|
||||
"server": "Server Address",
|
||||
"email": "Email",
|
||||
|
@ -60,6 +60,7 @@
|
||||
"login": {
|
||||
"login": "Connexion",
|
||||
"register": "Créer un compte",
|
||||
"add-account": "Ajouter un compte",
|
||||
"logout": "Déconnexion",
|
||||
"server": "Addresse du serveur",
|
||||
"email": "Email",
|
||||
|
Loading…
x
Reference in New Issue
Block a user