mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add more menus everywhere
This commit is contained in:
parent
c67a11b8b6
commit
e8bd663ad7
@ -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",
|
||||
)
|
||||
|
@ -21,6 +21,10 @@
|
||||
import { Platform } from "react-native";
|
||||
import { px } from "yoshiki/native";
|
||||
|
||||
export const important = <T,>(value: T): T => {
|
||||
return `${value} !important` as T;
|
||||
}
|
||||
|
||||
export const ts = (spacing: number) => {
|
||||
return px(spacing * 8);
|
||||
};
|
||||
|
@ -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 (
|
||||
<Link
|
||||
href={href}
|
||||
href={moreOpened ? undefined : href}
|
||||
onLongPress={() => 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 = ({
|
||||
>
|
||||
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
|
||||
{type === "movie" && watchPercent && <ItemProgress watchPercent={watchPercent} />}
|
||||
{slug && watchStatus !== undefined && type && type !== "collection" && (
|
||||
<ItemContext
|
||||
type={type}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
{...css([
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.dark.background,
|
||||
},
|
||||
"more",
|
||||
Platform.OS === "web" && moreOpened && { display: important("flex") },
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</PosterBackground>
|
||||
<Skeleton>
|
||||
{isLoading || (
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<ImageBackground
|
||||
@ -63,11 +69,8 @@ export const ItemList = ({
|
||||
alt={name}
|
||||
quality="medium"
|
||||
as={Link}
|
||||
href={href ?? ""}
|
||||
onFocus={() => 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,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "column",
|
||||
width: { xs: "50%", lg: "30%" },
|
||||
})}
|
||||
>
|
||||
<Skeleton {...css({ height: rem(2), alignSelf: "center" })}>
|
||||
{isLoading || (
|
||||
<Heading
|
||||
{...css({
|
||||
textAlign: "center",
|
||||
fontSize: rem(2),
|
||||
letterSpacing: rem(0.002),
|
||||
fontWeight: "900",
|
||||
textTransform: "uppercase",
|
||||
textDecorationLine: isHovered ? "underline" : "none",
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
>
|
||||
<Skeleton {...css({ height: rem(2), alignSelf: "center" })}>
|
||||
{isLoading || (
|
||||
<Heading
|
||||
{...css([
|
||||
"title",
|
||||
{
|
||||
textAlign: "center",
|
||||
fontSize: rem(2),
|
||||
letterSpacing: rem(0.002),
|
||||
fontWeight: "900",
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
])}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
)}
|
||||
</Skeleton>
|
||||
{slug && watchStatus !== undefined && type && type !== "collection" && (
|
||||
<ItemContext
|
||||
type={type}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => 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) },
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</Skeleton>
|
||||
</View>
|
||||
{(isLoading || subtitle) && (
|
||||
<Skeleton {...css({ width: rem(5), alignSelf: "center" })}>
|
||||
{isLoading || (
|
||||
<P
|
||||
{...css({
|
||||
textAlign: "center",
|
||||
marginRight: ts(4),
|
||||
})}
|
||||
>
|
||||
{subtitle}
|
||||
|
@ -160,6 +160,8 @@ export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
{(x) => (
|
||||
<ItemDetails
|
||||
isLoading={x.isLoading as any}
|
||||
slug={x.slug}
|
||||
type={x.kind?.toLowerCase() as any}
|
||||
name={x.name}
|
||||
tagline={"tagline" in x ? x.tagline : null}
|
||||
overview={x.overview}
|
||||
|
@ -28,13 +28,17 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { watchListIcon } from "./watchlist-info";
|
||||
|
||||
export const EpisodesContext = ({
|
||||
showSlug,
|
||||
type = "episode",
|
||||
slug,
|
||||
showSlug,
|
||||
status,
|
||||
...props
|
||||
}: { showSlug?: string | null; slug: string; status: WatchStatusV | null } & Partial<
|
||||
ComponentProps<typeof Menu<typeof IconButton>>
|
||||
>) => {
|
||||
}: {
|
||||
type?: "show" | "movie" | "episode";
|
||||
showSlug?: string | null;
|
||||
slug: string;
|
||||
status: WatchStatusV | null;
|
||||
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
||||
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 = ({
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const ItemContext = ({
|
||||
type,
|
||||
slug,
|
||||
status,
|
||||
...props
|
||||
}: {
|
||||
type: "movie" | "show";
|
||||
slug: string;
|
||||
status: WatchStatusV | null;
|
||||
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
||||
return <EpisodesContext type={type} slug={slug} status={status} showSlug={null} {...props} />;
|
||||
};
|
||||
|
@ -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 (
|
||||
<Link
|
||||
href={href}
|
||||
href={moreOpened ? undefined : href}
|
||||
onLongPress={() => 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) && (
|
||||
<ItemProgress watchPercent={watchedPercent ?? 100} />
|
||||
)}
|
||||
{slug && watchedStatus !== undefined && (
|
||||
<EpisodesContext
|
||||
slug={slug}
|
||||
showSlug={showSlug}
|
||||
status={watchedStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
{...css([
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bg: (theme) => theme.darkOverlay,
|
||||
},
|
||||
"more",
|
||||
Platform.OS === "web" && moreOpened && { display: important("flex") },
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</ImageBackground>
|
||||
<Skeleton {...css({ width: percent(50) })}>
|
||||
{isLoading || (
|
||||
@ -177,8 +210,7 @@ export const EpisodeLine = ({
|
||||
watchedPercent: number | null;
|
||||
watchedStatus: WatchStatusV | null;
|
||||
href: string;
|
||||
}> &
|
||||
Partial<PressableProps>) => {
|
||||
}>) => {
|
||||
const [moreOpened, setMoreOpened] = useState(false);
|
||||
const { css } = useYoshiki("episode-line");
|
||||
const { t } = useTranslation();
|
||||
@ -206,7 +238,7 @@ export const EpisodeLine = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
props as any,
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<P {...css({ width: rem(4), flexShrink: 0, m: ts(1), textAlign: "center" })}>
|
||||
@ -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") },
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
|
@ -57,6 +57,8 @@ export const NewsList = () => {
|
||||
) : (
|
||||
<EpisodeBox
|
||||
isLoading={x.isLoading as any}
|
||||
slug={x.slug}
|
||||
showSlug={x.showId}
|
||||
name={
|
||||
x.kind === NewsKind.Episode
|
||||
? `${x.show!.name} ${episodeDisplayNumber(x)}`
|
||||
|
@ -49,9 +49,13 @@ import { Layout, WithLoading } from "../fetch";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { ItemGrid, ItemWatchStatus } from "../browse/grid";
|
||||
import { useState } from "react";
|
||||
import { ItemContext } from "../components/context-menus";
|
||||
|
||||
export const ItemDetails = ({
|
||||
isLoading,
|
||||
slug,
|
||||
type,
|
||||
name,
|
||||
tagline,
|
||||
subtitle,
|
||||
@ -64,6 +68,8 @@ export const ItemDetails = ({
|
||||
unseenEpisodesCount,
|
||||
...props
|
||||
}: WithLoading<{
|
||||
slug: string;
|
||||
type: "movie" | "show" | "collection";
|
||||
name: string;
|
||||
tagline: string | null;
|
||||
subtitle: string;
|
||||
@ -75,8 +81,9 @@ export const ItemDetails = ({
|
||||
watchStatus: WatchStatusV | null;
|
||||
unseenEpisodesCount: number | null;
|
||||
}>) => {
|
||||
const { t } = useTranslation();
|
||||
const [moreOpened, setMoreOpened] = useState(false);
|
||||
const { css } = useYoshiki("recommanded-card");
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
@ -88,7 +95,8 @@ export const ItemDetails = ({
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={href}
|
||||
href={moreOpened ? undefined : href}
|
||||
onLongPress={() => setMoreOpened(true)}
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
@ -149,11 +157,22 @@ export const ItemDetails = ({
|
||||
<View
|
||||
{...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end", marginBottom: px(50) })}
|
||||
>
|
||||
{(isLoading || tagline) && (
|
||||
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
||||
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
|
||||
</Skeleton>
|
||||
)}
|
||||
<View {...css({ flexDirection: "row-reverse", justifyContent: "space-between" })}>
|
||||
{slug && type && type !== "collection" && watchStatus !== undefined && (
|
||||
<ItemContext
|
||||
type={type}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
/>
|
||||
)}
|
||||
{(isLoading || tagline) && (
|
||||
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
||||
{isLoading || <P {...css({ p: ts(1) })}>{tagline}</P>}
|
||||
</Skeleton>
|
||||
)}
|
||||
</View>
|
||||
<ScrollView {...css({ pX: ts(1) })}>
|
||||
<Skeleton lines={5} {...css({ height: rem(0.8) })}>
|
||||
{isLoading || (
|
||||
@ -231,6 +250,8 @@ export const Recommanded = () => {
|
||||
{(x) => (
|
||||
<ItemDetails
|
||||
isLoading={x.isLoading as any}
|
||||
slug={x.slug}
|
||||
type={x.kind?.toLowerCase() as any}
|
||||
name={x.name}
|
||||
tagline={"tagline" in x ? x.tagline : null}
|
||||
overview={x.overview}
|
||||
|
@ -61,11 +61,14 @@ export const WatchlistList = () => {
|
||||
(x.isLoading && i % 2) ? (
|
||||
<EpisodeBox
|
||||
isLoading={x.isLoading as any}
|
||||
slug={episode?.slug}
|
||||
showSlug={x.slug}
|
||||
name={episode ? `${x.name} ${episodeDisplayNumber(episode)}` : undefined}
|
||||
overview={episode?.name}
|
||||
thumbnail={episode?.thumbnail ?? x.thumbnail}
|
||||
href={episode?.href}
|
||||
watchedPercent={x.watchStatus?.watchedPercent || null}
|
||||
watchedStatus={x.watchStatus?.status || null}
|
||||
// TODO: Move this into the ItemList (using getItemSize)
|
||||
// @ts-expect-error This is a web only property
|
||||
{...css({ gridColumnEnd: "span 2" })}
|
||||
|
Loading…
x
Reference in New Issue
Block a user