Add watch status indicator on items list and recommanded card

This commit is contained in:
Zoe Roux 2023-12-05 00:48:00 +01:00
parent e5dde472c9
commit c24c8914bf
5 changed files with 114 additions and 51 deletions

View File

@ -22,7 +22,6 @@ import { KyooImage, WatchStatusV } from "@kyoo/models";
import {
Link,
Skeleton,
Poster,
ts,
focusReset,
P,
@ -31,7 +30,7 @@ import {
Icon,
} from "@kyoo/primitives";
import { ImageStyle, View } from "react-native";
import { percent, px, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { percent, px, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
@ -41,14 +40,16 @@ export const ItemGrid = ({
subtitle,
poster,
isLoading,
watchInfo,
watchStatus,
unseenEpisodesCount,
...props
}: WithLoading<{
href: string;
name: string;
subtitle?: string;
poster?: KyooImage | null;
watchInfo: WatchStatusV | string | null;
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
}> &
Stylable<"text">) => {
const { css } = useYoshiki("grid");
@ -89,7 +90,7 @@ export const ItemGrid = ({
layout={{ width: percent(100) }}
{...(css("poster") as { style: ImageStyle })}
>
{watchInfo && (
{(watchStatus === WatchStatusV.Completed || unseenEpisodesCount) && (
<View
{...css({
position: "absolute",
@ -104,10 +105,10 @@ export const ItemGrid = ({
borderRadius: 999999,
})}
>
{watchInfo === WatchStatusV.Completed ? (
{watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ m: 0, textAlign: "center" })}>{watchInfo}</P>
<P {...css({ m: 0, textAlign: "center" })}>{unseenEpisodesCount}</P>
)}
</View>
)}

View File

@ -25,7 +25,6 @@ import {
LibraryItemP,
ItemKind,
getDisplayDate,
WatchStatusV,
} from "@kyoo/models";
import { ComponentProps, useState } from "react";
import { createParam } from "solito";
@ -42,16 +41,7 @@ const { useParam } = createParam<{ sortBy?: string }>();
export const itemMap = (
item: WithLoading<LibraryItem>,
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
if (item.isLoading) return item;
let watchInfo: string | WatchStatusV | null = null;
if (item.kind !== ItemKind.Collection && item.watchStatus?.status === WatchStatusV.Completed)
watchInfo = WatchStatusV.Completed;
else if (item.kind === ItemKind.Show) {
if (!item.watchStatus) watchInfo = item.episodesCount!.toString();
else if (item.watchStatus.status === WatchStatusV.Watching)
watchInfo = item.watchStatus.unseenEpisodesCount.toString();
}
if (item.isLoading) return item as any;
return {
isLoading: item.isLoading,
@ -60,7 +50,11 @@ export const itemMap = (
href: item.href,
poster: item.poster,
thumbnail: item.thumbnail,
watchInfo: watchInfo,
watchStatus: item.kind !== ItemKind.Collection ? item.watchStatus?.status ?? null : null,
unseenEpisodesCount:
item.kind === ItemKind.Show
? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!
: null,
};
};

View File

@ -18,12 +18,23 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooImage } from "@kyoo/models";
import { Link, P, Skeleton, ts, ImageBackground, Poster, Heading } from "@kyoo/primitives";
import { KyooImage, WatchStatusV } from "@kyoo/models";
import {
Link,
P,
Skeleton,
ts,
ImageBackground,
Poster,
Heading,
Icon,
PosterBackground,
} from "@kyoo/primitives";
import { useState } from "react";
import { View } from "react-native";
import { percent, px, rem, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
export const ItemList = ({
href,
@ -32,12 +43,17 @@ export const ItemList = ({
thumbnail,
poster,
isLoading,
watchStatus,
unseenEpisodesCount,
...props
}: WithLoading<{
href: string;
name: string;
subtitle?: string;
poster?: KyooImage | null;
thumbnail?: KyooImage | null;
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
}>) => {
const { css } = useYoshiki();
const [isHovered, setHovered] = useState(0);
@ -59,14 +75,17 @@ export const ItemList = ({
imageStyle={{
borderRadius: px(6),
}}
{...css({
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
height: ItemList.layout.size,
borderRadius: px(6),
marginX: ItemList.layout.gap,
})}
{...css(
{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
height: ItemList.layout.size,
borderRadius: px(6),
marginX: ItemList.layout.gap,
},
props,
)}
>
<View
{...css({
@ -104,13 +123,36 @@ export const ItemList = ({
</Skeleton>
)}
</View>
<Poster
<PosterBackground
src={poster}
alt=""
quality="low"
forcedLoading={isLoading}
layout={{ height: percent(80) }}
/>
>
{(watchStatus === WatchStatusV.Completed || unseenEpisodesCount) && (
<View
{...css({
position: "absolute",
top: 0,
left: 0,
minWidth: ts(3.5),
aspectRatio: 1,
justifyContent: "center",
m: ts(0.5),
pX: ts(0.5),
bg: (theme) => theme.darkOverlay,
borderRadius: 999999,
})}
>
{watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ m: 0, textAlign: "center" })}>{unseenEpisodesCount}</P>
)}
</View>
)}
</PosterBackground>
</ImageBackground>
);
};

View File

@ -25,15 +25,17 @@ import {
LibraryItem,
LibraryItemP,
QueryIdentifier,
WatchStatusV,
getDisplayDate,
} from "@kyoo/models";
import {
Chip,
H3,
Icon,
IconFab,
ImageBackground,
Link,
P,
PosterBackground,
Skeleton,
SubP,
focusReset,
@ -48,6 +50,7 @@ import { Layout, WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import { ItemGrid } from "../browse/grid";
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
export const ItemDetails = ({
isLoading,
@ -59,6 +62,8 @@ export const ItemDetails = ({
genres,
href,
playHref,
watchStatus,
unseenEpisodesCount,
...props
}: WithLoading<{
name: string;
@ -69,6 +74,8 @@ export const ItemDetails = ({
overview: string | null;
href: string;
playHref: string | null;
watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null;
}>) => {
const { push } = useRouter();
const { t } = useTranslation();
@ -100,13 +107,12 @@ export const ItemDetails = ({
props,
)}
>
<ImageBackground
<PosterBackground
src={poster}
alt=""
quality="low"
gradient={false}
forcedLoading={isLoading}
{...css({ height: percent(100), aspectRatio: 2 / 3 })}
layout={{ height: percent(100) }}
>
<View
{...css({
@ -127,7 +133,29 @@ export const ItemDetails = ({
</Skeleton>
)}
</View>
</ImageBackground>
{(watchStatus === WatchStatusV.Completed || unseenEpisodesCount) && (
<View
{...css({
position: "absolute",
top: 0,
right: 0,
minWidth: ts(3.5),
aspectRatio: 1,
justifyContent: "center",
m: ts(0.5),
pX: ts(0.5),
bg: (theme) => theme.darkOverlay,
borderRadius: 999999,
})}
>
{watchStatus === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ m: 0, textAlign: "center" })}>{unseenEpisodesCount}</P>
)}
</View>
)}
</PosterBackground>
<View {...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end" })}>
{(isLoading || tagline) && (
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
@ -195,7 +223,7 @@ export const Recommanded = () => {
fetchMore={false}
{...css({ padding: 0 })}
>
{(x, i) => (
{(x) => (
<ItemDetails
isLoading={x.isLoading as any}
name={x.name}
@ -208,6 +236,14 @@ export const Recommanded = () => {
genres={"genres" in x ? x.genres : null}
href={x.href}
playHref={x.kind !== ItemKind.Collection && !x.isLoading ? x.playHref : undefined}
watchStatus={
!x.isLoading && x.kind !== ItemKind.Collection ? x.watchStatus?.status ?? null : null
}
unseenEpisodesCount={
x.kind === ItemKind.Show
? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount!
: null
}
/>
)}
</InfiniteFetch>
@ -222,6 +258,6 @@ Recommanded.query = (): QueryIdentifier<LibraryItem> => ({
params: {
sortBy: "random",
limit: 6,
fields: ["firstEpisode"],
fields: ["firstEpisode", "episodesCount", "watchStatus"],
},
});

View File

@ -26,6 +26,7 @@ import { InfiniteFetch } from "../fetch-infinite";
import { ItemList } from "../browse/list";
import { useTranslation } from "react-i18next";
import { ItemGrid } from "../browse/grid";
import { itemMap } from "../browse";
export const VerticalRecommanded = () => {
const { t } = useTranslation();
@ -40,19 +41,7 @@ export const VerticalRecommanded = () => {
layout={{ ...ItemList.layout, layout: "vertical" }}
fetchMore={false}
>
{(x, i) => (
<ItemList
key={x.id ?? i}
isLoading={x.isLoading as any}
href={x.href}
name={x.name}
subtitle={
x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined
}
poster={x.poster}
thumbnail={x.thumbnail}
/>
)}
{(x, i) => <ItemList key={x.id ?? i} {...itemMap(x)} />}
</InfiniteFetch>
</View>
);
@ -63,6 +52,7 @@ VerticalRecommanded.query = (): QueryIdentifier<LibraryItem> => ({
infinite: true,
path: ["items"],
params: {
fields: ["episodesCount", "watchStatus"],
sortBy: "random",
limit: 3,
},