Add submenues for watchlist status

This commit is contained in:
Zoe Roux 2023-12-14 14:12:54 +01:00
parent 8b0faf8a0b
commit 10924e2410
5 changed files with 172 additions and 38 deletions

View File

@ -27,6 +27,7 @@ import {
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { StyleSheet, Pressable, View } from "react-native";
@ -44,6 +45,8 @@ import { SvgProps } from "react-native-svg";
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
type Optional<T, K extends keyof any> = Omit<T, K> & Partial<T>;
const Menu = <AsProps,>({
Trigger,
onMenuOpen,
@ -59,20 +62,31 @@ const Menu = <AsProps,>({
onMenuClose?: () => void;
isOpen?: boolean;
setOpen?: (v: boolean) => void;
} & Omit<AsProps, "onPress">) => {
} & Optional<AsProps, "onPress">) => {
const insets = useSafeAreaInsets();
const alreadyRendered = useRef(false);
const [isOpen, setOpen] =
// eslint-disable-next-line react-hooks/rules-of-hooks
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
// deos the same as a useMemo but for props.
const memoRef = useRef({ onMenuOpen, onMenuClose });
memoRef.current = { onMenuOpen, onMenuClose };
useEffect(() => {
if (isOpen) onMenuOpen?.call(null);
else onMenuClose?.call(null);
}, [isOpen, onMenuClose, onMenuOpen]);
if (isOpen) memoRef.current.onMenuOpen?.();
else if (alreadyRendered.current) memoRef.current.onMenuClose?.();
alreadyRendered.current = true;
}, [isOpen]);
return (
<>
<Trigger onPress={() => setOpen(true)} {...(props as any)} />
<Trigger
onPress={() => {
if ("onPress" in props && typeof props.onPress === "function") props.onPress();
setOpen(true);
}}
{...(props as any)}
/>
{isOpen && (
<Portal>
<ContrastArea mode="user">
@ -195,4 +209,24 @@ const MenuItem = ({
};
Menu.Item = MenuItem;
const Sub = <AsProps,>({
children,
...props
}: {
label: string;
selected?: boolean;
left?: ReactElement;
disabled?: boolean;
icon?: ComponentType<SvgProps>;
children?: ReactNode | ReactNode[] | null;
} & AsProps) => {
const setOpen = useContext(MenuContext);
return (
<Menu Trigger={MenuItem} onMenuClose={() => setOpen?.(false)} {...props}>
{children}
</Menu>
);
};
Menu.Sub = Sub;
export { Menu };

View File

@ -66,7 +66,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
modal
open={isOpen}
onOpenChange={(newOpen) => {
if (setOpen) setOpen(newOpen)
if (setOpen) setOpen(newOpen);
if (newOpen) onMenuOpen?.call(null);
else onMenuClose?.call(null);
}}
@ -77,7 +77,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
<ContrastArea mode="user">
<SwitchVariant>
<YoshikiProvider>
{({ css }) => (
{({ css, theme }) => (
<DropdownMenu.Portal>
<DropdownMenu.Content
onFocusOutside={(e) => e.stopImmediatePropagation()}
@ -93,6 +93,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
})}
>
{children}
<DropdownMenu.Arrow fill={theme.background} />
</DropdownMenu.Content>
</DropdownMenu.Portal>
)}
@ -106,10 +107,10 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
const Item = forwardRef<
HTMLDivElement,
ComponentProps<typeof DropdownMenu.Item> & { href?: string }
>(function _Item({ children, href, ...props }, ref) {
>(function _Item({ children, href, onSelect, ...props }, ref) {
if (href) {
return (
<DropdownMenu.Item ref={ref} {...props} asChild>
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props} asChild>
<Link href={href} style={{ textDecoration: "none" }}>
{children}
</Link>
@ -117,28 +118,22 @@ const Item = forwardRef<
);
}
return (
<DropdownMenu.Item ref={ref} {...props}>
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props}>
{children}
</DropdownMenu.Item>
);
});
const MenuItem = ({
label,
icon,
left,
selected,
onSelect,
href,
disabled,
...props
}: {
label: string;
icon?: ComponentType<SvgProps>;
left?: ReactElement;
disabled?: boolean;
selected?: boolean;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
const MenuItem = forwardRef<
HTMLDivElement,
{
label: string;
icon?: ComponentType<SvgProps>;
left?: ReactElement;
disabled?: boolean;
selected?: boolean;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })
>(function MenuItem({ label, icon, left, selected, onSelect, href, disabled, ...props }, ref) {
const { css: nCss } = useNativeYoshiki();
const { css, theme } = useYoshiki();
@ -159,6 +154,7 @@ const MenuItem = ({
}
`}</style>
<Item
ref={ref}
onSelect={onSelect}
href={href}
disabled={disabled}
@ -190,7 +186,49 @@ const MenuItem = ({
</Item>
</>
);
};
});
Menu.Item = MenuItem;
const Sub = <AsProps,>({
children,
disabled,
...props
}: {
label: string;
selected?: boolean;
left?: ReactElement;
disabled?: boolean;
icon?: ComponentType<SvgProps>;
children: ReactNode | ReactNode[] | null;
} & AsProps) => {
const { css, theme } = useYoshiki();
return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger asChild disabled={disabled}>
<MenuItem disabled={disabled} {...props} onSelect={(e?: any) => e.preventDefault()} />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent
onFocusOutside={(e) => e.stopImmediatePropagation()}
{...css({
bg: (theme) => theme.background,
overflow: "auto",
minWidth: "220px",
borderRadius: "8px",
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
zIndex: 2,
maxHeight: "calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
})}
>
{children}
<DropdownMenu.Arrow fill={theme.background} />
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
);
};
Menu.Sub = Sub;
export { Menu };

View File

@ -23,16 +23,58 @@ import { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import Info from "@material-symbols/svg-400/rounded/info.svg";
import { WatchStatusV, queryFn, useAccount } from "@kyoo/models";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { watchListIcon } from "./watchlist-info";
export const EpisodesContext = ({
showSlug,
slug,
status,
...props
}: { showSlug: string } & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
}: { showSlug?: string; slug: string; status: WatchStatusV | null } & Partial<
ComponentProps<typeof Menu<typeof IconButton>>
>) => {
const account = useAccount();
const { t } = useTranslation();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newStatus: WatchStatusV | null) =>
queryFn({
path: ["episode", slug, "watchStatus", newStatus && `?status=${newStatus}`],
method: newStatus ? "POST" : "DELETE",
}),
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["episode", slug] }),
});
return (
<Menu Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))} {...(props as any)}>
<Menu.Item label={t("home.episodeMore.goToShow")} icon={Info} href={`/show/${showSlug}`} />
<Menu.Item
label={t("home.episodeMore.goToShow")}
icon={Info}
onSelect={() => console.log("tot")}
/>
{showSlug && (
<Menu.Item label={t("home.episodeMore.goToShow")} icon={Info} href={`/show/${showSlug}`} />
)}
<Menu.Sub
label={account ? t("show.watchlistEdit") : t("show.watchlistLogin")}
disabled={!account}
icon={watchListIcon(status)}
>
{Object.values(WatchStatusV).map((x) => (
<Menu.Item
key={x}
label={t(`show.watchlistMark.${x.toLowerCase()}`)}
onSelect={() => mutation.mutate(x)}
selected={x === status}
/>
))}
{status !== null && (
<Menu.Item label={t("show.watchlistMark.null")} onSelect={() => mutation.mutate(null)} />
)}
</Menu.Sub>
</Menu>
);
};

View File

@ -28,6 +28,19 @@ import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.sv
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { WatchStatusV, queryFn, useAccount } from "@kyoo/models";
export const watchListIcon = (status: WatchStatusV | null) => {
switch (status) {
case null:
return BookmarkAdd;
case WatchStatusV.Completed:
return BookmarkAdded;
case WatchStatusV.Droped:
return BookmarkRemove;
default:
return Bookmark;
}
};
export const WatchListInfo = ({
type,
slug,
@ -84,7 +97,7 @@ export const WatchListInfo = ({
return (
<Menu
Trigger={IconButton}
icon={status === WatchStatusV.Droped ? BookmarkRemove : Bookmark}
icon={watchListIcon(status)}
{...tooltip(t("show.watchlistEdit"))}
{...props}
>

View File

@ -144,6 +144,7 @@ export const EpisodeBox = ({
export const EpisodeLine = ({
slug,
showSlug,
displayNumber,
name,
thumbnail,
@ -161,6 +162,7 @@ export const EpisodeLine = ({
}: WithLoading<{
id: string;
slug: string;
showSlug: string;
displayNumber: string;
name: string | null;
overview: string | null;
@ -263,14 +265,19 @@ export const EpisodeLine = ({
</View>
<View {...css({ flexDirection: "row" })}>
<Skeleton>{isLoading || <P numberOfLines={3}>{overview}</P>}</Skeleton>
<EpisodesContext
isOpen={moreOpened}
setOpen={setMoreOpened}
{...css([
"more",
Platform.OS === "web" && moreOpened && { display: "flex !important" as any },
])}
/>
{slug && watchedStatus !== undefined && (
<EpisodesContext
slug={slug}
showSlug={showSlug}
status={watchedStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
{...css([
"more",
Platform.OS === "web" && moreOpened && { display: "flex !important" as any },
])}
/>
)}
</View>
</View>
</Link>