Cleanup context menues for shows

This commit is contained in:
2026-06-14 12:45:54 +02:00
parent 5454d29449
commit fc6e6f58fe
8 changed files with 206 additions and 215 deletions
+103 -48
View File
@@ -1,17 +1,21 @@
// import Download from "@material-symbols/svg-400/rounded/download.svg";
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
import Download from "@material-symbols/svg-400/rounded/download.svg";
import Info from "@material-symbols/svg-400/rounded/info.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
import VideoLibrary from "@material-symbols/svg-400/rounded/video_library-fill.svg";
import { useRouter } from "expo-router";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { Alert } from "react-native";
import { WatchStatusV } from "~/models";
import { HR, IconButton, Menu, tooltip } from "~/primitives";
import { HRP, IconButton, Menu, tooltip } from "~/primitives";
import { useAccount } from "~/providers/account-context";
import { useMutation } from "~/query";
import { cn } from "~/utils";
import { watchListIcon } from "./watchlist-info";
// import { useDownloader } from "../../packages/ui/src/downloads/ui/src/downloads";
export const EntryContext = ({
kind,
@@ -28,7 +32,6 @@ export const EntryContext = ({
className?: string;
} & Partial<ComponentProps<typeof Menu>> &
Partial<ComponentProps<typeof IconButton>>) => {
// const downloader = useDownloader();
const account = useAccount();
const { t } = useTranslation();
@@ -70,39 +73,46 @@ export const EntryContext = ({
onSelect={() => markAsSeenMutation.mutate()}
/>
)}
{/* <Menu.Item */}
{/* label={t("home.episodeMore.download")} */}
{/* icon={Download} */}
{/* onSelect={() => downloader(type, slug)} */}
{/* /> */}
{videoSlug && (
<Menu.Item
label={t("home.episodeMore.mediainfo")}
icon={MovieInfo}
href={`/info/${videoSlug}`}
/>
<>
<Menu.Item
label={t("home.episodeMore.download")}
icon={Download}
href={`/api/videos/${videoSlug}/direct`}
download
/>
<Menu.Item
label={t("home.episodeMore.mediainfo")}
icon={MovieInfo}
href={`/info/${videoSlug}`}
/>
</>
)}
</Menu>
);
};
export const ItemContext = ({
export const ShowContext = ({
kind,
slug,
name,
videoSlug,
status,
showWatchlist = true,
className,
...props
}: {
kind: "movie" | "serie";
kind: "movie" | "serie" | "collection";
slug: string;
name: string;
videoSlug: string | null;
status: WatchStatusV | null;
showWatchlist?: boolean;
className?: string;
} & Partial<ComponentProps<typeof Menu>> &
Partial<ComponentProps<typeof IconButton>>) => {
const account = useAccount();
// const downloader = useDownloader();
const router = useRouter();
const { t } = useTranslation();
const mutation = useMutation({
@@ -120,6 +130,12 @@ export const ItemContext = ({
invalidate: null,
});
const deleteMutation = useMutation({
method: "DELETE",
path: ["api", `${kind}s`, slug],
invalidate: ["api", "shows"],
});
return (
<Menu
Trigger={IconButton}
@@ -128,35 +144,38 @@ export const ItemContext = ({
{...tooltip(t("misc.more"))}
{...(props as any)}
>
<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() as Lowercase<WatchStatusV>}`,
)}
onSelect={() => mutation.mutate(x)}
selected={x === status}
/>
))}
{status !== null && (
<Menu.Item
label={t("show.watchlistMark.null")}
onSelect={() => mutation.mutate(null)}
/>
)}
</Menu.Sub>
{showWatchlist && kind !== "collection" && (
<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() as Lowercase<WatchStatusV>}`,
)}
onSelect={() => mutation.mutate(x)}
selected={x === status}
/>
))}
{status !== null && (
<Menu.Item
label={t("show.watchlistMark.null")}
onSelect={() => mutation.mutate(null)}
/>
)}
</Menu.Sub>
)}
{videoSlug && (
<>
{/* <Menu.Item */}
{/* label={t("home.episodeMore.download")} */}
{/* icon={Download} */}
{/* onSelect={() => downloader(type, slug)} */}
{/* /> */}
<Menu.Item
label={t("home.episodeMore.download")}
icon={Download}
href={`/api/videos/${videoSlug}/direct`}
download
/>
<Menu.Item
label={t("home.episodeMore.mediainfo")}
icon={MovieInfo}
@@ -166,11 +185,47 @@ export const ItemContext = ({
)}
{account?.isAdmin === true && (
<>
<HR />
<HRP text={t("navbar.admin")} />
<Menu.Item
label={t("home.refreshMetadata")}
icon={Refresh}
onSelect={() => metadataRefreshMutation.mutate()}
label={t("show.videos-map")}
icon={VideoLibrary}
href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`}
/>
{kind !== "collection" && (
<Menu.Item
label={t("show.remap")}
icon={Search}
href={`/${kind}s/${slug}/remap?q=${name}`}
/>
)}
{kind !== "collection" && (
<Menu.Item
label={t("home.refreshMetadata")}
icon={Refresh}
onSelect={() => metadataRefreshMutation.mutate()}
/>
)}
<Menu.Item
label={t("misc.delete")}
icon={Delete}
onSelect={() => {
Alert.alert(
t("misc.delete-name", { name }),
t("login.delete-confirmation"),
[
{ text: t("misc.cancel"), style: "cancel" },
{
text: t("misc.delete"),
style: "destructive",
onPress: async () => {
await deleteMutation.mutateAsync();
router.back();
},
},
],
{ cancelable: true },
);
}}
/>
</>
)}
+10 -11
View File
@@ -2,7 +2,7 @@ import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View, type ViewProps } from "react-native";
import { ItemContext } from "~/components/items/context-menus";
import { ShowContext } from "~/components/items/context-menus";
import { ItemWatchStatus } from "~/components/items/item-helpers";
import type { Genre, KImage, WatchStatusV } from "~/models";
import {
@@ -85,16 +85,15 @@ export const ItemDetails = ({
</PosterBackground>
<View className="mb-14 flex-1 justify-end">
<View className="my-2 flex-row-reverse justify-between p-2">
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
videoSlug={videoSlug}
/>
)}
<ShowContext
kind={kind}
slug={slug}
name={name}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
videoSlug={videoSlug}
/>
{tagline && <P className="p-1">{tagline}</P>}
</View>
<ScrollView className="p-3">
+16 -17
View File
@@ -12,7 +12,7 @@ import {
} from "~/primitives";
import type { Layout } from "~/query";
import { cn } from "~/utils";
import { ItemContext } from "./context-menus";
import { ShowContext } from "./context-menus";
import { ItemWatchStatus } from "./item-helpers";
export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => {
@@ -87,22 +87,21 @@ export const ItemGrid = ({
{kind === "movie" && !!watchPercent && (
<ItemProgress watchPercent={watchPercent} />
)}
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
videoSlug={videoSlug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
className={cn(
"absolute top-0 right-0 bg-gray-800/70 hover:bg-gray-800 focus-visible:bg-gray-800",
"native:hidden opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
moreOpened && "opacity-100",
)}
iconClassName="fill-slate-200 dark:fill-slate-200"
/>
)}
<ShowContext
kind={kind}
slug={slug}
name={name}
videoSlug={videoSlug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
className={cn(
"absolute top-0 right-0 bg-gray-800/70 hover:bg-gray-800 focus-visible:bg-gray-800",
"native:hidden opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
moreOpened && "opacity-100",
)}
iconClassName="fill-slate-200 dark:fill-slate-200"
/>
</PosterBackground>
<P
numberOfLines={subtitle ? 1 : 2}
+17 -18
View File
@@ -13,7 +13,7 @@ import {
} from "~/primitives";
import type { Layout } from "~/query";
import { cn } from "~/utils";
import { ItemContext } from "./context-menus";
import { ShowContext } from "./context-menus";
import { ItemWatchStatus } from "./item-helpers";
export const ItemList = ({
@@ -73,23 +73,22 @@ export const ItemList = ({
>
{name}
</Heading>
{kind !== "collection" && (
<ItemContext
kind={kind}
slug={slug}
videoSlug={videoSlug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
className={cn(
"ml-4",
"bg-gray-800/70 hover:bg-gray-800 focus-visible:bg-gray-800",
"native:hidden opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
moreOpened && "opacity-100",
)}
iconClassName="fill-slate-200 dark:fill-slate-200"
/>
)}
<ShowContext
kind={kind}
slug={slug}
name={name}
videoSlug={videoSlug}
status={watchStatus}
isOpen={moreOpened}
setOpen={(v) => setMoreOpened(v)}
className={cn(
"ml-4",
"bg-gray-800/70 hover:bg-gray-800 focus-visible:bg-gray-800",
"native:hidden opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
moreOpened && "opacity-100",
)}
iconClassName="fill-slate-200 dark:fill-slate-200"
/>
</View>
{subtitle && (
<P className="mr-8 text-center text-slate-400">{subtitle}</P>
+9 -3
View File
@@ -15,9 +15,11 @@ import { P } from "./text";
export function useLinkTo({
href,
replace = false,
download,
}: {
href?: string | null;
replace?: boolean;
download?: boolean;
}) {
const router = useRouter();
@@ -28,8 +30,9 @@ export function useLinkTo({
return {
// @ts-expect-error href is not known
href: href,
download,
onPress: (e) => {
if (e?.defaultPrevented) return;
if (e?.defaultPrevented || download) return;
// prevent native navigation via href.
e?.preventDefault();
if (href.startsWith("http")) {
@@ -46,15 +49,17 @@ export function useLinkTo({
export const A = ({
href,
replace,
download,
children,
className,
...props
}: TextProps & {
href?: string | null;
replace?: boolean;
download?: boolean;
children: ReactNode;
}) => {
const linkProps = useLinkTo({ href, replace });
const linkProps = useLinkTo({ href, replace, download });
return (
<P
@@ -94,6 +99,7 @@ export const PressableFeedback = ({
export const Link = ({
href,
replace,
download,
children,
disabled,
...props
@@ -102,7 +108,7 @@ export const Link = ({
replace?: boolean;
download?: boolean;
} & PressableProps) => {
const linkProps = useLinkTo({ href, replace });
const linkProps = useLinkTo({ href, replace, download });
return (
<PressableFeedback
+2 -2
View File
@@ -117,8 +117,8 @@ const MenuItem = ({
closeOnSelect?: boolean;
icon?: ComponentType<SvgProps>;
} & (
| { onSelect: () => void; href?: undefined }
| { href: string; onSelect?: undefined }
| { onSelect: () => void; href?: undefined; download?: undefined }
| { href: string; download?: boolean; onSelect?: undefined }
)) => {
const setOpen = useContext(MenuContext);
const router = useRouter();
+34 -18
View File
@@ -85,8 +85,8 @@ const MenuItem = forwardRef<
closeOnSelect?: boolean;
className?: string;
} & (
| { onSelect: () => void; href?: undefined }
| { href: string; onSelect?: undefined }
| { onSelect: () => void; href?: undefined; download?: undefined }
| { href: string; download?: boolean; onSelect?: undefined }
)
>(function MenuItem(
{
@@ -96,6 +96,7 @@ const MenuItem = forwardRef<
selected,
onSelect,
href,
download,
disabled,
closeOnSelect = true,
className,
@@ -114,7 +115,36 @@ const MenuItem = forwardRef<
/>
);
const { onPress, ...linkProps } = useLinkTo({ href });
let content = (
<>
{left && left}
{!left && icn}
<P
className={cn(
"flex-1 group-data-highlighted:text-slate-200",
disabled && "text-slate-600",
)}
style={{
paddingLeft: 8 * 2 + +!(icon || selected || left) * 24,
}}
>
{label}
</P>
{left && icn}
</>
);
let { onPress, ...linkProps } = useLinkTo({ href });
if (href && download) {
content = (
<a href={href} download className="flew-row flex items-center">
{content}
</a>
);
onPress = undefined;
linkProps = {};
}
return (
<DropdownMenu.Item
@@ -132,21 +162,7 @@ const MenuItem = forwardRef<
)}
{...props}
>
{left && left}
{!left && icn}
<P
className={cn(
"flex-1 group-data-highlighted:text-slate-200",
disabled && "text-slate-600",
)}
style={{
paddingLeft: 8 * 2 + +!(icon || selected || left) * 24,
}}
>
{label}
</P>
{left && icn}
{content}
</DropdownMenu.Item>
);
});
+15 -98
View File
@@ -1,18 +1,12 @@
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
import VideoLibrary from "@material-symbols/svg-400/rounded/video_library-fill.svg";
import { useRouter } from "expo-router";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import { entryDisplayNumber } from "~/components/entries";
import { EntrySelect } from "~/components/entries/select";
import { ShowContext } from "~/components/items/context-menus";
import { WatchListInfo } from "~/components/items/watchlist-info";
import { Rating } from "~/components/rating";
import {
@@ -26,7 +20,6 @@ import {
import type { Metadata } from "~/models/utils/metadata";
import {
A,
Alert,
Chip,
Container,
capitalize,
@@ -40,7 +33,6 @@ import {
ImageBackground,
LI,
Link,
Menu,
P,
Popup,
Poster,
@@ -49,8 +41,7 @@ import {
UL,
usePopup,
} from "~/primitives";
import { useAccount } from "~/providers/account-context";
import { Fetch, type QueryIdentifier, useMutation } from "~/query";
import { Fetch, type QueryIdentifier } from "~/query";
import { cn, displayRuntime, getDisplayDate } from "~/utils";
import { PartOf } from "./part-of";
@@ -64,6 +55,7 @@ const ButtonList = ({
trailerUrl,
watchStatus,
iconsClassName,
videoSlug,
}: {
kind: "movie" | "serie" | "collection";
slug: string;
@@ -74,22 +66,10 @@ const ButtonList = ({
trailerUrl: string | null;
watchStatus: WatchStatusV | null;
iconsClassName?: string;
videoSlug: string | null;
}) => {
const account = useAccount();
const { t } = useTranslation();
const router = useRouter();
const [setPopup, closePopup] = usePopup();
const deleteMutation = useMutation({
method: "DELETE",
path: ["api", `${kind}s`, slug],
invalidate: ["api", "shows"],
});
const metadataRefreshMutation = useMutation({
method: "POST",
path: ["scanner", `${kind}s`, slug, "refresh"],
invalidate: null,
});
return (
<View className="flex-row items-center justify-center">
@@ -135,76 +115,15 @@ const ButtonList = ({
iconClassName={iconsClassName}
/>
)}
{(kind === "movie" || account?.isAdmin === true) && (
<Menu
Trigger={IconButton}
icon={MoreHoriz}
iconClassName={iconsClassName}
{...tooltip(t("misc.more"))}
>
{kind === "movie" && videos?.length === 1 && (
<>
{/* <Menu.Item */}
{/* icon={Download} */}
{/* onSelect={() => downloader(kind, slug)} */}
{/* label={t("home.episodeMore.download")} */}
{/* /> */}
<Menu.Item
label={t("home.episodeMore.mediainfo")}
icon={MovieInfo}
href={`/info/${slug}`}
/>
{account?.isAdmin && <HR />}
</>
)}
{account?.isAdmin === true && (
<>
<Menu.Item
label={t("show.videos-map")}
icon={VideoLibrary}
href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`}
/>
{kind !== "collection" && (
<Menu.Item
label={t("show.remap")}
icon={Search}
href={`/${kind}s/${slug}/remap?q=${name}`}
/>
)}
{kind !== "collection" && <HR />}
{kind !== "collection" && (
<Menu.Item
label={t("home.refreshMetadata")}
icon={Refresh}
onSelect={() => metadataRefreshMutation.mutate()}
/>
)}
<Menu.Item
label={t("misc.delete")}
icon={Delete}
onSelect={() => {
Alert.alert(
t("misc.delete-name", { name }),
t("login.delete-confirmation"),
[
{ text: t("misc.cancel"), style: "cancel" },
{
text: t("misc.delete"),
style: "destructive",
onPress: async () => {
await deleteMutation.mutateAsync();
router.back();
},
},
],
{ cancelable: true },
);
}}
/>
</>
)}
</Menu>
)}
<ShowContext
kind={kind}
slug={slug}
name={name}
videoSlug={videoSlug}
status={watchStatus}
showWatchlist={false}
iconClassName={iconsClassName}
/>
</View>
);
};
@@ -272,6 +191,7 @@ export const TitleLine = ({
trailerUrl={trailerUrl}
watchStatus={watchStatus}
iconsClassName="lg:fill-slate-200 dark:fill-slate-200"
videoSlug={videos?.length === 1 ? videos[0].slug : null}
/>
{Object.keys(rating).length > 0 && (
<>
@@ -317,10 +237,7 @@ TitleLine.Loader = ({
<View className="flex-warp flex-row items-center max-sm:justify-center sm:mt-8">
<IconFab icon={PlayArrow} iconClassName="lg:fill-slate-200" />
<IconButton icon={Theaters} iconClassName="lg:fill-slate-200" />
{kind !== "collection" && (
<IconButton icon={BookmarkAdd} iconClassName="lg:fill-slate-200" />
)}
{kind === "movie" && <IconButton icon={MoreHoriz} />}
<IconButton icon={MoreHoriz} iconClassName="lg:fill-slate-200" />
<DottedSeparator className="lg:text-slate-200" />
<Rating.Loader
textClassName="lg:text-slate-200"