Add watch status indicator on items grid

This commit is contained in:
Zoe Roux 2023-12-05 00:06:11 +01:00
parent 0bc6512bcc
commit 7ccfab4d8b
6 changed files with 76 additions and 16 deletions

View File

@ -21,7 +21,7 @@
import { ImageStyle, View, ViewProps, ViewStyle } from "react-native";
import { Props, ImageLayout, YoshikiEnhanced } from "./base-image";
import { Image } from "./image";
import { ComponentType, ReactNode } from "react";
import { ComponentProps, ComponentType, ReactNode } from "react";
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
import { ContrastArea } from "../themes";
import { percent } from "yoshiki/native";
@ -37,6 +37,22 @@ export const Poster = ({
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
export const PosterBackground = ({
alt,
layout,
...props
}: Omit<ComponentProps<typeof ImageBackground>, "layout"> & { style?: ImageStyle } & {
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => (
<ImageBackground
alt={alt!}
layout={{ aspectRatio: 2 / 3, ...layout }}
hideLoad={false}
gradient={false}
{...props}
/>
);
export const ImageBackground = <AsProps = ViewProps,>({
src,
alt,

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ComponentType, ComponentProps, ReactNode } from "react";
import { ComponentType, ComponentProps } from "react";
import { Platform, Text, TextProps, TextStyle, StyleProp } from "react-native";
import { percent, rem, useYoshiki } from "yoshiki/native";
import {

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ThemeBuilder, alpha } from "./theme";
import { ThemeBuilder } from "./theme";
// Ref: https://github.com/catppuccin/catppuccin
export const catppuccin: ThemeBuilder = {

View File

@ -18,11 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooImage } from "@kyoo/models";
import { Link, Skeleton, Poster, ts, focusReset, P, SubP } from "@kyoo/primitives";
import { ImageStyle } from "react-native";
import { percent, px, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { KyooImage, WatchStatusV } from "@kyoo/models";
import {
Link,
Skeleton,
Poster,
ts,
focusReset,
P,
SubP,
PosterBackground,
Icon,
} from "@kyoo/primitives";
import { ImageStyle, View } from "react-native";
import { 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";
export const ItemGrid = ({
href,
@ -30,12 +41,14 @@ export const ItemGrid = ({
subtitle,
poster,
isLoading,
watchInfo,
...props
}: WithLoading<{
href: string;
name: string;
subtitle?: string;
poster?: KyooImage | null;
watchInfo: WatchStatusV | string | null;
}> &
Stylable<"text">) => {
const { css } = useYoshiki("grid");
@ -68,14 +81,37 @@ export const ItemGrid = ({
props,
)}
>
<Poster
<PosterBackground
src={poster}
alt={name}
quality="low"
forcedLoading={isLoading}
layout={{ width: percent(100) }}
{...(css("poster") as { style: ImageStyle })}
/>
>
{watchInfo && (
<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,
})}
>
{watchInfo === WatchStatusV.Completed ? (
<Icon icon={Done} size={16} />
) : (
<P {...css({ m: 0, textAlign: "center" })}>{watchInfo}</P>
)}
</View>
)}
</PosterBackground>
<Skeleton>
{isLoading || (
<P numberOfLines={1} {...css([{ marginY: 0, textAlign: "center" }, "title"])}>

View File

@ -25,6 +25,7 @@ import {
LibraryItemP,
ItemKind,
getDisplayDate,
WatchStatusV,
} from "@kyoo/models";
import { ComponentProps, useState } from "react";
import { createParam } from "solito";
@ -43,6 +44,15 @@ export const itemMap = (
): 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();
}
return {
isLoading: item.isLoading,
name: item.name,
@ -50,6 +60,7 @@ export const itemMap = (
href: item.href,
poster: item.poster,
thumbnail: item.thumbnail,
watchInfo: watchInfo,
};
};
@ -59,6 +70,7 @@ const query = (sortKey?: SortBy, sortOrd?: SortOrd): QueryIdentifier<LibraryItem
infinite: true,
params: {
sortBy: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc",
fields: ["watchStatus", "episodesCount"],
},
});

View File

@ -36,6 +36,7 @@ import ChevronLeft from "@material-symbols/svg-400/rounded/chevron_left-fill.svg
import ChevronRight from "@material-symbols/svg-400/rounded/chevron_right-fill.svg";
import { InfiniteFetch, InfiniteFetchList } from "../fetch-infinite";
import { useTranslation } from "react-i18next";
import { itemMap } from "../browse";
export const Header = ({ title }: { title: string }) => {
const { css } = useYoshiki();
@ -85,13 +86,7 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
return (
<ItemGrid
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}
{...itemMap(x)}
/>
);
}}
@ -105,6 +100,7 @@ GenreGrid.query = (genre: Genre): QueryIdentifier<LibraryItem> => ({
infinite: true,
path: ["items"],
params: {
fields: ["watchStatus", "episodesCount"],
filter: `genres has ${genre}`,
sortBy: "random",
// Limit the inital numbers of items