mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add logout button for logged in users
This commit is contained in:
parent
98a0466761
commit
ea53eb9b24
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { getSecureItem, setSecureItem } from "./secure-store";
|
||||
import { deleteSecureItem, getSecureItem, setSecureItem } from "./secure-store";
|
||||
import { zdate } from "./utils";
|
||||
import { queryFn } from "./query";
|
||||
import { KyooErrors } from "./kyoo-errors";
|
||||
@ -79,3 +79,7 @@ export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [n
|
||||
export const getToken = async (cookies?: string): Promise<string | null> =>
|
||||
(await getTokenWJ(cookies))[0]
|
||||
|
||||
|
||||
export const logout = async () =>{
|
||||
deleteSecureItem("auth")
|
||||
}
|
||||
|
@ -18,4 +18,8 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export { setItemAsync as setSecureItem, getItemAsync as getSecureItem } from "expo-secure-store";
|
||||
export {
|
||||
setItemAsync as setSecureItem,
|
||||
deleteItemAsync as deleteSecureItem,
|
||||
getItemAsync as getSecureItem,
|
||||
} from "expo-secure-store";
|
||||
|
@ -18,7 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const setSecureItemSync = (key: string, value?: string) => {
|
||||
export const setSecureItemSync = (key: string, value: string | null) => {
|
||||
const d = new Date();
|
||||
// A year
|
||||
d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
@ -27,9 +27,11 @@ export const setSecureItemSync = (key: string, value?: string) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const setSecureItem = async (key: string, value: string): Promise<null> =>
|
||||
export const setSecureItem = async (key: string, value: string | null): Promise<null> =>
|
||||
setSecureItemSync(key, value);
|
||||
|
||||
export const deleteSecureItem = async (key: string) => setSecureItem(key, null);
|
||||
|
||||
export const getSecureItem = async (key: string, cookies?: string): Promise<string | null> => {
|
||||
// Don't try to use document's cookies on SSR.
|
||||
if (!cookies && typeof window === "undefined") return null;
|
||||
|
@ -25,31 +25,30 @@ import { Icon } from "./icons";
|
||||
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||
import { YoshikiStyle } from "yoshiki/dist/type";
|
||||
import { P } from "@expo/html-elements";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export const Avatar = ({
|
||||
src,
|
||||
alt,
|
||||
size = px(24),
|
||||
color,
|
||||
placeholder,
|
||||
isLoading = false,
|
||||
fill = false,
|
||||
...props
|
||||
}: {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
size?: YoshikiStyle<number | string>;
|
||||
placeholder?: string;
|
||||
color?: string;
|
||||
isLoading?: boolean;
|
||||
fill?: boolean;
|
||||
} & Stylable) => {
|
||||
export const Avatar = forwardRef<
|
||||
View,
|
||||
{
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
size?: YoshikiStyle<number | string>;
|
||||
placeholder?: string;
|
||||
color?: string;
|
||||
isLoading?: boolean;
|
||||
fill?: boolean;
|
||||
} & Stylable
|
||||
>(function _Avatar(
|
||||
{ src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, ...props },
|
||||
ref,
|
||||
) {
|
||||
const { css, theme } = useYoshiki();
|
||||
const col = color ?? theme.overlay0;
|
||||
|
||||
// TODO: Support dark themes when fill === true
|
||||
return (
|
||||
<View
|
||||
ref={ref}
|
||||
{...css(
|
||||
[
|
||||
{ borderRadius: 999999, width: size, height: size, overflow: "hidden" },
|
||||
@ -76,4 +75,4 @@ export const Avatar = ({
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -30,20 +30,21 @@ import { P } from "./text";
|
||||
import { ContrastArea } from "./themes";
|
||||
import { ts } from "./utils";
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import { useRouter } from "solito/router";
|
||||
|
||||
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
|
||||
|
||||
const Menu = <AsProps,>({
|
||||
Triger,
|
||||
Trigger,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
Triger: ComponentType<AsProps>;
|
||||
Trigger: ComponentType<AsProps>;
|
||||
children: ReactNode | ReactNode[] | null;
|
||||
onMenuOpen: () => void;
|
||||
onMenuClose: () => void;
|
||||
onMenuOpen?: () => void;
|
||||
onMenuClose?: () => void;
|
||||
} & Omit<AsProps, "onPress">) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
@ -55,7 +56,7 @@ const Menu = <AsProps,>({
|
||||
return (
|
||||
<>
|
||||
{/* @ts-ignore */}
|
||||
<Triger onPress={() => setOpen(true)} {...props} />
|
||||
<Trigger onPress={() => setOpen(true)} {...props} />
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<ContrastArea mode="user">
|
||||
@ -112,20 +113,22 @@ const MenuItem = ({
|
||||
label,
|
||||
selected,
|
||||
onSelect,
|
||||
href,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
onSelect: () => void;
|
||||
}) => {
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const setOpen = useContext(MenuContext);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
setOpen?.call(null, false);
|
||||
onSelect?.call(null);
|
||||
if (href) router.push(href);
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
|
@ -19,7 +19,8 @@
|
||||
*/
|
||||
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { ComponentType, forwardRef, ReactNode } from "react";
|
||||
import { ComponentProps, ComponentType, forwardRef, ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { PressableProps } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/web";
|
||||
import { px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
|
||||
@ -43,16 +44,16 @@ const InternalTriger = forwardRef<unknown, any>(function _Triger(
|
||||
});
|
||||
|
||||
const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
||||
Triger,
|
||||
Trigger,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
Triger: ComponentType<AsProps>;
|
||||
Trigger: ComponentType<AsProps>;
|
||||
children: ReactNode | ReactNode[] | null;
|
||||
onMenuOpen: () => void;
|
||||
onMenuClose: () => void;
|
||||
onMenuOpen?: () => void;
|
||||
onMenuClose?: () => void;
|
||||
} & Omit<AsProps, "onPress">) => {
|
||||
return (
|
||||
<DropdownMenu.Root
|
||||
@ -63,7 +64,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<InternalTriger Component={Triger} ComponentProps={props} />
|
||||
<InternalTriger Component={Trigger} ComponentProps={props} />
|
||||
</DropdownMenu.Trigger>
|
||||
<ContrastArea mode="user">
|
||||
<YoshikiProvider>
|
||||
@ -90,18 +91,38 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
||||
);
|
||||
};
|
||||
|
||||
const Item = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentProps<typeof DropdownMenu.Item> & { href?: string }
|
||||
>(function _Item({ children, href, ...props }, ref) {
|
||||
if (href) {
|
||||
return (
|
||||
<DropdownMenu.Item ref={ref} {...props} asChild>
|
||||
<Link href={href} style={{ textDecoration: "none" }}>
|
||||
{children}
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DropdownMenu.Item ref={ref} {...props}>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
const MenuItem = ({
|
||||
label,
|
||||
icon,
|
||||
selected,
|
||||
onSelect,
|
||||
href,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
icon?: JSX.Element;
|
||||
selected?: boolean;
|
||||
onSelect: () => void;
|
||||
}) => {
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||
const { css: nCss } = useNativeYoshiki();
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
@ -112,8 +133,9 @@ const MenuItem = ({
|
||||
background: ${theme.alternate.accent};
|
||||
}
|
||||
`}</style>
|
||||
<DropdownMenu.Item
|
||||
<Item
|
||||
onSelect={onSelect}
|
||||
href={href}
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
@ -121,7 +143,6 @@ const MenuItem = ({
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
height: "32px",
|
||||
color: (theme) => theme.paragraph,
|
||||
focus: {
|
||||
boxShadow: "none",
|
||||
},
|
||||
@ -138,8 +159,8 @@ const MenuItem = ({
|
||||
{...nCss({ paddingRight: px(8) })}
|
||||
/>
|
||||
)}
|
||||
{<P {...nCss([{ color: "inherit" }, !selected && { paddingLeft: px(8 * 2) }])}>{label}</P>}
|
||||
</DropdownMenu.Item>
|
||||
{<P {...nCss(!selected && { paddingLeft: px(8 * 2) })}>{label}</P>}
|
||||
</Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { FormPage } from "./form";
|
||||
import { PasswordInput } from "./password-input";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const LoginPage: QueryPage = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
@ -36,6 +37,7 @@ export const LoginPage: QueryPage = () => {
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
@ -66,7 +68,9 @@ export const LoginPage: QueryPage = () => {
|
||||
onPress={async () => {
|
||||
const { error } = await loginFunc("login", { username, password });
|
||||
setError(error);
|
||||
if (!error) router.push("/");
|
||||
if (error) return;
|
||||
queryClient.invalidateQueries(["auth", "me"]);
|
||||
router.push("/");
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
|
@ -29,6 +29,7 @@ import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { FormPage } from "./form";
|
||||
import { PasswordInput } from "./password-input";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const RegisterPage: QueryPage = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
@ -38,6 +39,7 @@ export const RegisterPage: QueryPage = () => {
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
@ -81,7 +83,9 @@ export const RegisterPage: QueryPage = () => {
|
||||
onPress={async () => {
|
||||
const { error } = await loginFunc("register", { email, username, password });
|
||||
setError(error);
|
||||
if (!error) router.push("/");
|
||||
if (error) return;
|
||||
queryClient.invalidateQueries(["auth", "me"]);
|
||||
router.push("/");
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
|
@ -18,7 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Library, LibraryP, Page, Paged, QueryIdentifier, User, UserP } from "@kyoo/models";
|
||||
import { Library, LibraryP, logout, Page, Paged, QueryIdentifier, User, UserP } from "@kyoo/models";
|
||||
import {
|
||||
Input,
|
||||
IconButton,
|
||||
@ -29,17 +29,19 @@ import {
|
||||
tooltip,
|
||||
ts,
|
||||
Link,
|
||||
Menu,
|
||||
} from "@kyoo/primitives";
|
||||
import { Platform, TextInput, View, ViewProps } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createParam } from "solito";
|
||||
import { useRouter } from "solito/router";
|
||||
import { rem, Stylable, useYoshiki } from "yoshiki/native";
|
||||
import Menu 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 { Fetch, FetchNE } from "../fetch";
|
||||
import { KyooLongLogo } from "./icon";
|
||||
import { forwardRef, useRef, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }) => {
|
||||
const { t } = useTranslation();
|
||||
@ -90,23 +92,37 @@ export const MeQuery: QueryIdentifier<User> = {
|
||||
export const NavbarProfile = () => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// TODO: show logged in user.
|
||||
return (
|
||||
<FetchNE query={MeQuery}>
|
||||
{({ username }) => (
|
||||
<Link
|
||||
href="/login"
|
||||
{({ isError: isGuest, username }) => (
|
||||
<Menu
|
||||
Trigger={Avatar}
|
||||
placeholder={username}
|
||||
alt={t("navbar.login")}
|
||||
size={30}
|
||||
color={theme.colors.white}
|
||||
{...css({ marginLeft: ts(1), marginVertical: "auto", justifyContent: "center" })}
|
||||
{...tooltip(username ?? t("navbar.login"))}
|
||||
{...css({ marginLeft: ts(1), justifyContent: "center" })}
|
||||
>
|
||||
<Avatar
|
||||
placeholder={username}
|
||||
alt={t("navbar.login")}
|
||||
size={30}
|
||||
color={theme.colors.white}
|
||||
/>
|
||||
</Link>
|
||||
{isGuest ? (
|
||||
<>
|
||||
<Menu.Item label={t("login.login")} href="/login" />
|
||||
<Menu.Item label={t("login.register")} href="/register" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Menu.Item
|
||||
label={t("login.logout")}
|
||||
onSelect={() => {
|
||||
logout();
|
||||
queryClient.invalidateQueries(["auth", "me"]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</FetchNE>
|
||||
);
|
||||
@ -175,13 +191,12 @@ export const Navbar = (props: Stylable) => {
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
zIndex: 1,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
icon={Menu}
|
||||
icon={MenuIcon}
|
||||
aria-label="more"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
|
@ -65,7 +65,7 @@ export const RightButtons = ({
|
||||
<View {...css({ flexDirection: "row" }, props)}>
|
||||
{subtitles && (
|
||||
<Menu
|
||||
Triger={IconButton}
|
||||
Trigger={IconButton}
|
||||
icon={ClosedCaption}
|
||||
onMenuOpen={onMenuOpen}
|
||||
onMenuClose={onMenuClose}
|
||||
|
@ -53,6 +53,7 @@
|
||||
"login": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"logout": "Logout",
|
||||
"server": "Server Address",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
|
@ -53,6 +53,7 @@
|
||||
"login": {
|
||||
"login": "Connexion",
|
||||
"register": "Créer un compte",
|
||||
"logout": "Déconnexion",
|
||||
"server": "Addresse du serveur",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
|
Loading…
x
Reference in New Issue
Block a user