mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 04:04:21 -04:00
Add submenues for watchlist status
This commit is contained in:
parent
8b0faf8a0b
commit
10924e2410
@ -27,6 +27,7 @@ import {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { StyleSheet, Pressable, View } from "react-native";
|
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);
|
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
|
||||||
|
|
||||||
|
type Optional<T, K extends keyof any> = Omit<T, K> & Partial<T>;
|
||||||
|
|
||||||
const Menu = <AsProps,>({
|
const Menu = <AsProps,>({
|
||||||
Trigger,
|
Trigger,
|
||||||
onMenuOpen,
|
onMenuOpen,
|
||||||
@ -59,20 +62,31 @@ const Menu = <AsProps,>({
|
|||||||
onMenuClose?: () => void;
|
onMenuClose?: () => void;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
setOpen?: (v: boolean) => void;
|
setOpen?: (v: boolean) => void;
|
||||||
} & Omit<AsProps, "onPress">) => {
|
} & Optional<AsProps, "onPress">) => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const alreadyRendered = useRef(false);
|
||||||
const [isOpen, setOpen] =
|
const [isOpen, setOpen] =
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) onMenuOpen?.call(null);
|
if (isOpen) memoRef.current.onMenuOpen?.();
|
||||||
else onMenuClose?.call(null);
|
else if (alreadyRendered.current) memoRef.current.onMenuClose?.();
|
||||||
}, [isOpen, onMenuClose, onMenuOpen]);
|
alreadyRendered.current = true;
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
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 && (
|
{isOpen && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<ContrastArea mode="user">
|
<ContrastArea mode="user">
|
||||||
@ -195,4 +209,24 @@ const MenuItem = ({
|
|||||||
};
|
};
|
||||||
Menu.Item = 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 };
|
export { Menu };
|
||||||
|
@ -66,7 +66,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
|||||||
modal
|
modal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={(newOpen) => {
|
onOpenChange={(newOpen) => {
|
||||||
if (setOpen) setOpen(newOpen)
|
if (setOpen) setOpen(newOpen);
|
||||||
if (newOpen) onMenuOpen?.call(null);
|
if (newOpen) onMenuOpen?.call(null);
|
||||||
else onMenuClose?.call(null);
|
else onMenuClose?.call(null);
|
||||||
}}
|
}}
|
||||||
@ -77,7 +77,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
|||||||
<ContrastArea mode="user">
|
<ContrastArea mode="user">
|
||||||
<SwitchVariant>
|
<SwitchVariant>
|
||||||
<YoshikiProvider>
|
<YoshikiProvider>
|
||||||
{({ css }) => (
|
{({ css, theme }) => (
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onFocusOutside={(e) => e.stopImmediatePropagation()}
|
onFocusOutside={(e) => e.stopImmediatePropagation()}
|
||||||
@ -93,6 +93,7 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<DropdownMenu.Arrow fill={theme.background} />
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
)}
|
)}
|
||||||
@ -106,10 +107,10 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
|||||||
const Item = forwardRef<
|
const Item = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
ComponentProps<typeof DropdownMenu.Item> & { href?: string }
|
ComponentProps<typeof DropdownMenu.Item> & { href?: string }
|
||||||
>(function _Item({ children, href, ...props }, ref) {
|
>(function _Item({ children, href, onSelect, ...props }, ref) {
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Item ref={ref} {...props} asChild>
|
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props} asChild>
|
||||||
<Link href={href} style={{ textDecoration: "none" }}>
|
<Link href={href} style={{ textDecoration: "none" }}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
@ -117,28 +118,22 @@ const Item = forwardRef<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Item ref={ref} {...props}>
|
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const MenuItem = ({
|
const MenuItem = forwardRef<
|
||||||
label,
|
HTMLDivElement,
|
||||||
icon,
|
{
|
||||||
left,
|
label: string;
|
||||||
selected,
|
icon?: ComponentType<SvgProps>;
|
||||||
onSelect,
|
left?: ReactElement;
|
||||||
href,
|
disabled?: boolean;
|
||||||
disabled,
|
selected?: boolean;
|
||||||
...props
|
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })
|
||||||
}: {
|
>(function MenuItem({ label, icon, left, selected, onSelect, href, disabled, ...props }, ref) {
|
||||||
label: string;
|
|
||||||
icon?: ComponentType<SvgProps>;
|
|
||||||
left?: ReactElement;
|
|
||||||
disabled?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
|
||||||
const { css: nCss } = useNativeYoshiki();
|
const { css: nCss } = useNativeYoshiki();
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
|
||||||
@ -159,6 +154,7 @@ const MenuItem = ({
|
|||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Item
|
<Item
|
||||||
|
ref={ref}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
href={href}
|
href={href}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -190,7 +186,49 @@ const MenuItem = ({
|
|||||||
</Item>
|
</Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
Menu.Item = MenuItem;
|
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 };
|
export { Menu };
|
||||||
|
@ -23,16 +23,58 @@ import { ComponentProps } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
|
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
|
||||||
import Info from "@material-symbols/svg-400/rounded/info.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 = ({
|
export const EpisodesContext = ({
|
||||||
showSlug,
|
showSlug,
|
||||||
|
slug,
|
||||||
|
status,
|
||||||
...props
|
...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 { 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 (
|
return (
|
||||||
<Menu Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))} {...(props as any)}>
|
<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>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -28,6 +28,19 @@ import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.sv
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { WatchStatusV, queryFn, useAccount } from "@kyoo/models";
|
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 = ({
|
export const WatchListInfo = ({
|
||||||
type,
|
type,
|
||||||
slug,
|
slug,
|
||||||
@ -84,7 +97,7 @@ export const WatchListInfo = ({
|
|||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
Trigger={IconButton}
|
Trigger={IconButton}
|
||||||
icon={status === WatchStatusV.Droped ? BookmarkRemove : Bookmark}
|
icon={watchListIcon(status)}
|
||||||
{...tooltip(t("show.watchlistEdit"))}
|
{...tooltip(t("show.watchlistEdit"))}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -144,6 +144,7 @@ export const EpisodeBox = ({
|
|||||||
|
|
||||||
export const EpisodeLine = ({
|
export const EpisodeLine = ({
|
||||||
slug,
|
slug,
|
||||||
|
showSlug,
|
||||||
displayNumber,
|
displayNumber,
|
||||||
name,
|
name,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
@ -161,6 +162,7 @@ export const EpisodeLine = ({
|
|||||||
}: WithLoading<{
|
}: WithLoading<{
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
showSlug: string;
|
||||||
displayNumber: string;
|
displayNumber: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
overview: string | null;
|
overview: string | null;
|
||||||
@ -263,14 +265,19 @@ export const EpisodeLine = ({
|
|||||||
</View>
|
</View>
|
||||||
<View {...css({ flexDirection: "row" })}>
|
<View {...css({ flexDirection: "row" })}>
|
||||||
<Skeleton>{isLoading || <P numberOfLines={3}>{overview}</P>}</Skeleton>
|
<Skeleton>{isLoading || <P numberOfLines={3}>{overview}</P>}</Skeleton>
|
||||||
<EpisodesContext
|
{slug && watchedStatus !== undefined && (
|
||||||
isOpen={moreOpened}
|
<EpisodesContext
|
||||||
setOpen={setMoreOpened}
|
slug={slug}
|
||||||
{...css([
|
showSlug={showSlug}
|
||||||
"more",
|
status={watchedStatus}
|
||||||
Platform.OS === "web" && moreOpened && { display: "flex !important" as any },
|
isOpen={moreOpened}
|
||||||
])}
|
setOpen={(v) => setMoreOpened(v)}
|
||||||
/>
|
{...css([
|
||||||
|
"more",
|
||||||
|
Platform.OS === "web" && moreOpened && { display: "flex !important" as any },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user