mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add submenues for watchlist status
This commit is contained in:
parent
8b0faf8a0b
commit
10924e2410
@ -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 };
|
||||
|
@ -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 };
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user