2024-05-20 22:18:18 +02:00

250 lines
5.5 KiB
TypeScript

/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { type KyooImage, WatchStatusV } from "@kyoo/models";
import {
Icon,
Link,
P,
Poster,
PosterBackground,
Skeleton,
SubP,
focusReset,
important,
ts,
} from "@kyoo/primitives";
import Done from "@material-symbols/svg-400/rounded/check-fill.svg";
import { useState } from "react";
import { type ImageStyle, Platform, View } from "react-native";
import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native";
import { ItemContext } from "../components/context-menus";
import type { Layout } from "../fetch";
export const ItemWatchStatus = ({
watchStatus,
unseenEpisodesCount,
...props
}: {
watchStatus?: WatchStatusV | null;
unseenEpisodesCount?: number | null;
}) => {
const { css } = useYoshiki();
if (watchStatus !== WatchStatusV.Completed && !unseenEpisodesCount) return null;
return (
<View
{...css(
{
position: "absolute",
top: 0,
right: 0,
minWidth: max(rem(1), ts(3.5)),
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
m: ts(0.5),
pX: ts(0.5),
bg: (theme) => theme.darkOverlay,
borderRadius: 999999,
},
props,
)}
>
{watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ marginVertical: 0, verticalAlign: "middle", textAlign: "center" })}>
{unseenEpisodesCount}
</P>
)}
</View>
);
};
export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => {
const { css } = useYoshiki("episodebox");
return (
<>
<View
{...css({
backgroundColor: (theme) => theme.user.overlay0,
width: percent(100),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
<View
{...css({
backgroundColor: (theme) => theme.user.accent,
width: percent(watchPercent),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
</>
);
};
export const ItemGrid = ({
href,
slug,
name,
type,
subtitle,
poster,
watchStatus,
watchPercent,
unseenEpisodesCount,
...props
}: {
href: string;
slug: string;
name: string;
subtitle: string | null;
poster: KyooImage | null;
watchStatus: WatchStatusV | null;
watchPercent: number | null;
type: "movie" | "show" | "collection";
unseenEpisodesCount: number | null;
} & Stylable<"text">) => {
const [moreOpened, setMoreOpened] = useState(false);
const { css } = useYoshiki("grid");
return (
<Link
href={moreOpened ? undefined : href}
onLongPress={() => setMoreOpened(true)}
{...css(
{
flexDirection: "column",
alignItems: "center",
width: percent(100),
child: {
poster: {
borderColor: (theme) => theme.background,
borderWidth: ts(0.5),
borderStyle: "solid",
},
more: {
display: "none",
},
},
fover: {
self: focusReset,
poster: {
borderColor: (theme: Theme) => theme.accent,
},
title: {
textDecorationLine: "underline",
},
more: {
display: "flex",
},
},
},
props,
)}
>
<PosterBackground
src={poster}
alt={name}
quality="low"
layout={{ width: percent(100) }}
{...(css("poster") as { style: ImageStyle })}
>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
{type === "movie" && watchPercent && <ItemProgress watchPercent={watchPercent} />}
{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>
<P numberOfLines={subtitle ? 1 : 2} {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
{name}
</P>
{subtitle && (
<SubP
{...css({
marginTop: 0,
textAlign: "center",
})}
>
{subtitle}
</SubP>
)}
</Link>
);
};
ItemGrid.Loader = (props: Stylable) => {
const { css } = useYoshiki();
return (
<View
{...css(
{
flexDirection: "column",
alignItems: "center",
width: percent(100),
},
props,
)}
>
<Poster.Loader
layout={{ width: percent(100) }}
{...css({
borderColor: (theme) => theme.background,
borderWidth: ts(0.5),
borderStyle: "solid",
})}
/>
<Skeleton />
<Skeleton {...css({ width: percent(50) })} />
</View>
);
};
ItemGrid.layout = {
size: px(150),
numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 },
gap: { xs: ts(1), sm: ts(2), md: ts(4) },
layout: "grid",
} satisfies Layout;