diff --git a/front/packages/models/src/resources/episode.base.ts b/front/packages/models/src/resources/episode.base.ts index f961f9e0..6da1d810 100644 --- a/front/packages/models/src/resources/episode.base.ts +++ b/front/packages/models/src/resources/episode.base.ts @@ -67,6 +67,10 @@ export const BaseEpisodeP = withImages( */ hls: z.string().transform(imageFn), }), + /** + * The id of the show containing this episode + */ + showId: z.string(), }), "episodes", ) diff --git a/front/packages/primitives/src/utils/spacing.tsx b/front/packages/primitives/src/utils/spacing.tsx index 08c951fa..c253b0d7 100644 --- a/front/packages/primitives/src/utils/spacing.tsx +++ b/front/packages/primitives/src/utils/spacing.tsx @@ -21,6 +21,10 @@ import { Platform } from "react-native"; import { px } from "yoshiki/native"; +export const important = (value: T): T => { + return `${value} !important` as T; +} + export const ts = (spacing: number) => { return px(spacing * 8); }; diff --git a/front/packages/ui/src/browse/grid.tsx b/front/packages/ui/src/browse/grid.tsx index bbcf4c69..b74eb516 100644 --- a/front/packages/ui/src/browse/grid.tsx +++ b/front/packages/ui/src/browse/grid.tsx @@ -19,11 +19,13 @@ */ import { KyooImage, WatchStatusV } from "@kyoo/models"; -import { Link, Skeleton, ts, focusReset, P, SubP, PosterBackground, Icon } from "@kyoo/primitives"; -import { ImageStyle, View } from "react-native"; +import { Link, Skeleton, ts, focusReset, P, SubP, PosterBackground, Icon, important } from "@kyoo/primitives"; +import { ImageStyle, Platform, View } from "react-native"; import { max, percent, px, rem, Stylable, Theme, useYoshiki } from "yoshiki/native"; import { Layout, WithLoading } from "../fetch"; import Done from "@material-symbols/svg-400/rounded/done-fill.svg"; +import { ItemContext } from "../components/context-menus"; +import { useState } from "react"; export const ItemWatchStatus = ({ watchStatus, @@ -96,6 +98,7 @@ export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => { export const ItemGrid = ({ href, + slug, name, type, subtitle, @@ -107,6 +110,7 @@ export const ItemGrid = ({ ...props }: WithLoading<{ href: string; + slug: string; name: string; subtitle?: string; poster?: KyooImage | null; @@ -116,11 +120,13 @@ export const ItemGrid = ({ unseenEpisodesCount: number | null; }> & Stylable<"text">) => { + const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("grid"); return ( setMoreOpened(true)} {...css( { flexDirection: "column", @@ -132,6 +138,9 @@ export const ItemGrid = ({ borderWidth: ts(0.5), borderStyle: "solid", }, + more: { + display: "none", + }, }, fover: { self: focusReset, @@ -141,6 +150,9 @@ export const ItemGrid = ({ title: { textDecorationLine: "underline", }, + more: { + display: "flex", + }, }, }, props, @@ -156,6 +168,25 @@ export const ItemGrid = ({ > {type === "movie" && watchPercent && } + {slug && watchStatus !== undefined && type && type !== "collection" && ( + setMoreOpened(v)} + {...css([ + { + position: "absolute", + top: 0, + right: 0, + bg: (theme) => theme.dark.background, + }, + "more", + Platform.OS === "web" && moreOpened && { display: important("flex") }, + ])} + /> + )} {isLoading || ( diff --git a/front/packages/ui/src/browse/index.tsx b/front/packages/ui/src/browse/index.tsx index a98534d8..ea28fd42 100644 --- a/front/packages/ui/src/browse/index.tsx +++ b/front/packages/ui/src/browse/index.tsx @@ -45,6 +45,7 @@ export const itemMap = ( return { isLoading: item.isLoading, + slug: item.slug, name: item.name, subtitle: item.kind !== ItemKind.Collection ? getDisplayDate(item) : undefined, href: item.href, diff --git a/front/packages/ui/src/browse/list.tsx b/front/packages/ui/src/browse/list.tsx index a717c6a9..138901ca 100644 --- a/front/packages/ui/src/browse/list.tsx +++ b/front/packages/ui/src/browse/list.tsx @@ -28,15 +28,19 @@ import { Heading, PosterBackground, imageBorderRadius, + important, } from "@kyoo/primitives"; import { useState } from "react"; -import { View } from "react-native"; +import { Platform, View } from "react-native"; import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { Layout, WithLoading } from "../fetch"; import { ItemWatchStatus } from "./grid"; +import { ItemContext } from "../components/context-menus"; export const ItemList = ({ href, + slug, + type, name, subtitle, thumbnail, @@ -47,6 +51,8 @@ export const ItemList = ({ ...props }: WithLoading<{ href: string; + slug: string; + type: "movie" | "show" | "collection"; name: string; subtitle?: string; poster?: KyooImage | null; @@ -55,7 +61,7 @@ export const ItemList = ({ unseenEpisodesCount: number | null; }>) => { const { css } = useYoshiki(); - const [isHovered, setHovered] = useState(0); + const [moreOpened, setMoreOpened] = useState(false); return ( setHovered((i) => i + 1)} - onBlur={() => setHovered((i) => i - 1)} - onPressIn={() => setHovered((i) => i + 1)} - onPressOut={() => setHovered((i) => i - 1)} + href={moreOpened ? undefined : href} + onLongPress={() => setMoreOpened(true)} containerStyle={{ borderRadius: px(imageBorderRadius), }} @@ -83,38 +86,78 @@ export const ItemList = ({ borderRadius: px(imageBorderRadius), overflow: "hidden", marginX: ItemList.layout.gap, + child: { + more: { + opacity: 0, + }, + }, + fover: { + title: { + textDecorationLine: "underline", + }, + more: { + opacity: 100, + }, + }, }, props, )} > - - {isLoading || ( - - {name} - + + + {isLoading || ( + + {name} + + )} + + {slug && watchStatus !== undefined && type && type !== "collection" && ( + setMoreOpened(v)} + {...css([ + { + // I dont know why marginLeft gets overwritten by the margin: px(2) so we important + marginLeft: important(ts(2)), + bg: (theme) => theme.darkOverlay, + }, + "more", + Platform.OS === "web" && moreOpened && { opacity: important(100) }, + ])} + /> )} - + {(isLoading || subtitle) && ( {isLoading || (

{subtitle} diff --git a/front/packages/ui/src/collection/index.tsx b/front/packages/ui/src/collection/index.tsx index 2e59ec8b..2eda6cae 100644 --- a/front/packages/ui/src/collection/index.tsx +++ b/front/packages/ui/src/collection/index.tsx @@ -160,6 +160,8 @@ export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => { {(x) => ( > ->) => { +}: { + type?: "show" | "movie" | "episode"; + showSlug?: string | null; + slug: string; + status: WatchStatusV | null; +} & Partial>>) => { const account = useAccount(); const { t } = useTranslation(); @@ -42,10 +46,10 @@ export const EpisodesContext = ({ const mutation = useMutation({ mutationFn: (newStatus: WatchStatusV | null) => queryFn({ - path: ["episode", slug, "watchStatus", newStatus && `?status=${newStatus}`], + path: [type, slug, "watchStatus", newStatus && `?status=${newStatus}`], method: newStatus ? "POST" : "DELETE", }), - onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["episode", slug] }), + onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }), }); return ( @@ -73,3 +77,16 @@ export const EpisodesContext = ({ ); }; + +export const ItemContext = ({ + type, + slug, + status, + ...props +}: { + type: "movie" | "show"; + slug: string; + status: WatchStatusV | null; +} & Partial>>) => { + return ; +}; diff --git a/front/packages/ui/src/details/episode.tsx b/front/packages/ui/src/details/episode.tsx index e702693a..0c4005b1 100644 --- a/front/packages/ui/src/details/episode.tsx +++ b/front/packages/ui/src/details/episode.tsx @@ -23,6 +23,7 @@ import { H6, ImageBackground, ImageProps, + important, Link, P, Skeleton, @@ -58,6 +59,8 @@ export const displayRuntime = (runtime: number) => { }; export const EpisodeBox = ({ + slug, + showSlug, name, overview, thumbnail, @@ -68,6 +71,9 @@ export const EpisodeBox = ({ ...props }: Stylable & WithLoading<{ + slug: string; + // if show slug is null, disable "Go to show" in the context menu + showSlug: string | null; name: string | null; overview: string | null; href: string; @@ -75,12 +81,14 @@ export const EpisodeBox = ({ watchedPercent: number | null; watchedStatus: WatchStatusV | null; }>) => { + const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("episodebox"); const { t } = useTranslation(); return ( setMoreOpened(true)} {...css( { alignItems: "center", @@ -90,6 +98,9 @@ export const EpisodeBox = ({ borderWidth: ts(0.5), borderStyle: "solid", }, + more: { + display: "none", + }, }, fover: { self: focusReset, @@ -99,6 +110,9 @@ export const EpisodeBox = ({ title: { textDecorationLine: "underline", }, + more: { + display: "flex", + }, }, }, props, @@ -117,6 +131,25 @@ export const EpisodeBox = ({ {(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( )} + {slug && watchedStatus !== undefined && ( + setMoreOpened(v)} + {...css([ + { + position: "absolute", + top: 0, + right: 0, + bg: (theme) => theme.darkOverlay, + }, + "more", + Platform.OS === "web" && moreOpened && { display: important("flex") }, + ])} + /> + )} {isLoading || ( @@ -177,8 +210,7 @@ export const EpisodeLine = ({ watchedPercent: number | null; watchedStatus: WatchStatusV | null; href: string; -}> & - Partial) => { +}>) => { const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("episode-line"); const { t } = useTranslation(); @@ -206,7 +238,7 @@ export const EpisodeLine = ({ }, }, }, - props as any, + props, )} >

@@ -277,7 +309,7 @@ export const EpisodeLine = ({ setOpen={(v) => setMoreOpened(v)} {...css([ "more", - Platform.OS === "web" && moreOpened && { display: "flex !important" as any }, + Platform.OS === "web" && moreOpened && { display: important("flex") }, ])} /> )} diff --git a/front/packages/ui/src/home/news.tsx b/front/packages/ui/src/home/news.tsx index f3b469cc..0e1e213a 100644 --- a/front/packages/ui/src/home/news.tsx +++ b/front/packages/ui/src/home/news.tsx @@ -57,6 +57,8 @@ export const NewsList = () => { ) : ( ) => { - const { t } = useTranslation(); + const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("recommanded-card"); + const { t } = useTranslation(); return ( setMoreOpened(true)} {...css({ position: "absolute", top: 0, @@ -149,11 +157,22 @@ export const ItemDetails = ({ - {(isLoading || tagline) && ( - - {isLoading ||

{tagline}

} -
- )} + + {slug && type && type !== "collection" && watchStatus !== undefined && ( + setMoreOpened(v)} + /> + )} + {(isLoading || tagline) && ( + + {isLoading ||

{tagline}

} +
+ )} +
{isLoading || ( @@ -231,6 +250,8 @@ export const Recommanded = () => { {(x) => ( { (x.isLoading && i % 2) ? (