diff --git a/front/src/components/items/context-menus.tsx b/front/src/components/items/context-menus.tsx index fdf27add3..206453e7e 100644 --- a/front/src/components/items/context-menus.tsx +++ b/front/src/components/items/context-menus.tsx @@ -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> & Partial>) => { - // const downloader = useDownloader(); const account = useAccount(); const { t } = useTranslation(); @@ -70,39 +73,46 @@ export const EntryContext = ({ onSelect={() => markAsSeenMutation.mutate()} /> )} - {/* downloader(type, slug)} */} - {/* /> */} {videoSlug && ( - + <> + + + )} ); }; -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> & Partial>) => { 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 ( - - {Object.values(WatchStatusV).map((x) => ( - }`, - )} - onSelect={() => mutation.mutate(x)} - selected={x === status} - /> - ))} - {status !== null && ( - mutation.mutate(null)} - /> - )} - + {showWatchlist && kind !== "collection" && ( + + {Object.values(WatchStatusV).map((x) => ( + }`, + )} + onSelect={() => mutation.mutate(x)} + selected={x === status} + /> + ))} + {status !== null && ( + mutation.mutate(null)} + /> + )} + + )} {videoSlug && ( <> - {/* downloader(type, slug)} */} - {/* /> */} + -
+ metadataRefreshMutation.mutate()} + label={t("show.videos-map")} + icon={VideoLibrary} + href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`} + /> + {kind !== "collection" && ( + + )} + {kind !== "collection" && ( + metadataRefreshMutation.mutate()} + /> + )} + { + 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 }, + ); + }} /> )} diff --git a/front/src/components/items/item-details.tsx b/front/src/components/items/item-details.tsx index c40bccaf2..fb0561b00 100644 --- a/front/src/components/items/item-details.tsx +++ b/front/src/components/items/item-details.tsx @@ -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 = ({ - {kind !== "collection" && ( - setMoreOpened(v)} - videoSlug={videoSlug} - /> - )} + setMoreOpened(v)} + videoSlug={videoSlug} + /> {tagline &&

{tagline}

}
diff --git a/front/src/components/items/item-grid.tsx b/front/src/components/items/item-grid.tsx index 36eeed9fc..c144a865f 100644 --- a/front/src/components/items/item-grid.tsx +++ b/front/src/components/items/item-grid.tsx @@ -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 && ( )} - {kind !== "collection" && ( - 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" - /> - )} + 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" + />

{name} - {kind !== "collection" && ( - 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" - /> - )} + 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" + /> {subtitle && (

{subtitle}

diff --git a/front/src/primitives/links.tsx b/front/src/primitives/links.tsx index 07745e679..70a622f50 100644 --- a/front/src/primitives/links.tsx +++ b/front/src/primitives/links.tsx @@ -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 (

{ - const linkProps = useLinkTo({ href, replace }); + const linkProps = useLinkTo({ href, replace, download }); return ( ; } & ( - | { 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(); diff --git a/front/src/primitives/menu.web.tsx b/front/src/primitives/menu.web.tsx index 0c4186dda..44da13af8 100644 --- a/front/src/primitives/menu.web.tsx +++ b/front/src/primitives/menu.web.tsx @@ -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} +

+ {label} +

+ {left && icn} + + ); + + let { onPress, ...linkProps } = useLinkTo({ href }); + + if (href && download) { + content = ( + + {content} + + ); + onPress = undefined; + linkProps = {}; + } return ( - {left && left} - {!left && icn} -

- {label} -

- - {left && icn} + {content}
); }); diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index 90668bb31..3a6bcb93d 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -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 ( @@ -135,76 +115,15 @@ const ButtonList = ({ iconClassName={iconsClassName} /> )} - {(kind === "movie" || account?.isAdmin === true) && ( - - {kind === "movie" && videos?.length === 1 && ( - <> - {/* downloader(kind, slug)} */} - {/* label={t("home.episodeMore.download")} */} - {/* /> */} - - {account?.isAdmin &&
} - - )} - {account?.isAdmin === true && ( - <> - - {kind !== "collection" && ( - - )} - {kind !== "collection" &&
} - {kind !== "collection" && ( - metadataRefreshMutation.mutate()} - /> - )} - { - 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 }, - ); - }} - /> - - )} -
- )} +
); }; @@ -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 = ({ - {kind !== "collection" && ( - - )} - {kind === "movie" && } +