Add watch percent display on episodes

This commit is contained in:
Zoe Roux 2023-12-04 22:27:44 +01:00
parent fe155898bb
commit e70174cb24
6 changed files with 104 additions and 43 deletions

View File

@ -18,11 +18,11 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useLayoutEffect, useState } from "react"; import { useState } from "react";
import { ImageStyle, View, ViewStyle } from "react-native"; import { ImageStyle, View, ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image"; import { Props, ImageLayout } from "./base-image";
import { BlurhashContainer, blurHashToDataURL } from "./blurhash.web"; import { BlurhashContainer } from "./blurhash.web";
import { Skeleton } from "../skeleton"; import { Skeleton } from "../skeleton";
import NextImage from "next/image"; import NextImage from "next/image";

View File

@ -19,7 +19,7 @@
*/ */
import { ImageStyle, View, ViewProps, ViewStyle } from "react-native"; import { ImageStyle, View, ViewProps, ViewStyle } from "react-native";
import { Props, YoshikiEnhanced } from "./base-image"; import { Props, ImageLayout, YoshikiEnhanced } from "./base-image";
import { Image } from "./image"; import { Image } from "./image";
import { ComponentType, ReactNode } from "react"; import { ComponentType, ReactNode } from "react";
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient"; import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
@ -47,6 +47,8 @@ export const ImageBackground = <AsProps = ViewProps,>({
containerStyle, containerStyle,
imageStyle, imageStyle,
forcedLoading, forcedLoading,
hideLoad = true,
layout,
...asProps ...asProps
}: { }: {
as?: ComponentType<AsProps>; as?: ComponentType<AsProps>;
@ -54,13 +56,20 @@ export const ImageBackground = <AsProps = ViewProps,>({
children: ReactNode; children: ReactNode;
containerStyle?: YoshikiEnhanced<ViewStyle>; containerStyle?: YoshikiEnhanced<ViewStyle>;
imageStyle?: YoshikiEnhanced<ImageStyle>; imageStyle?: YoshikiEnhanced<ImageStyle>;
hideLoad?: boolean;
layout?: ImageLayout;
} & AsProps & } & AsProps &
Props) => { Props) => {
const Container = as ?? View; const Container = as ?? View;
return ( return (
<ContrastArea contrastText> <ContrastArea contrastText>
{({ css, theme }) => ( {({ css, theme }) => (
<Container {...(asProps as AsProps)}> <Container
{...(css(
[layout, !hideLoad && { borderRadius: 6, overflow: "hidden" }],
asProps,
) as AsProps)}
>
<View <View
{...css([ {...css([
{ {
@ -75,14 +84,14 @@ export const ImageBackground = <AsProps = ViewProps,>({
containerStyle, containerStyle,
])} ])}
> >
{src && ( {(src || !hideLoad) && (
<Image <Image
src={src} src={src}
quality={quality} quality={quality}
forcedLoading={forcedLoading} forcedLoading={forcedLoading}
alt={alt!} alt={alt!}
layout={{ width: percent(100), height: percent(100) }} layout={{ width: percent(100), height: percent(100) }}
Error={null} Error={hideLoad ? null : undefined}
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as { {...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
style: ImageStyle; style: ImageStyle;
})} })}

View File

@ -18,12 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { focusReset, H6, Image, ImageProps, Link, P, Skeleton, SubP, ts } from "@kyoo/primitives"; import {
focusReset,
H6,
ImageBackground,
ImageProps,
Link,
P,
Skeleton,
SubP,
ts,
} from "@kyoo/primitives";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ImageStyle, View } from "react-native"; import { ImageStyle, View } from "react-native";
import { Layout, WithLoading } from "../fetch"; import { Layout, WithLoading } from "../fetch";
import { percent, px, rem, Stylable, Theme, useYoshiki } from "yoshiki/native"; import { percent, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { KyooImage } from "@kyoo/models"; import { KyooImage, WatchStatusV } from "@kyoo/models";
export const episodeDisplayNumber = ( export const episodeDisplayNumber = (
episode: { episode: {
@ -50,6 +60,8 @@ export const EpisodeBox = ({
thumbnail, thumbnail,
isLoading, isLoading,
href, href,
watchedPercent,
watchedStatus,
...props ...props
}: Stylable & }: Stylable &
WithLoading<{ WithLoading<{
@ -57,6 +69,8 @@ export const EpisodeBox = ({
overview: string | null; overview: string | null;
href: string; href: string;
thumbnail?: ImageProps["src"] | null; thumbnail?: ImageProps["src"] | null;
watchedPercent: number | null;
watchedStatus: WatchStatusV | null;
}>) => { }>) => {
const { css } = useYoshiki("episodebox"); const { css } = useYoshiki("episodebox");
const { t } = useTranslation(); const { t } = useTranslation();
@ -87,14 +101,39 @@ export const EpisodeBox = ({
props, props,
)} )}
> >
<Image <ImageBackground
src={thumbnail} src={thumbnail}
quality="low" quality="low"
alt="" alt=""
graditent={false}
hideLoad={false}
forcedLoading={isLoading} forcedLoading={isLoading}
layout={{ width: percent(100), aspectRatio: 16 / 9 }} layout={{ width: percent(100), aspectRatio: 16 / 9 }}
{...(css("poster") as any)} {...(css("poster") as any)}
/> >
{(watchedPercent || watchedStatus === WatchStatusV.Completed) && (
<>
<View
{...css({
backgroundColor: (theme) => theme.overlay0,
width: percent(100),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
<View
{...css({
backgroundColor: (theme) => theme.accent,
width: percent(watchedPercent ?? 100),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
</>
)}
</ImageBackground>
<Skeleton {...css({ width: percent(50) })}> <Skeleton {...css({ width: percent(50) })}>
{isLoading || ( {isLoading || (
<P {...css([{ marginY: 0, textAlign: "center" }, "title"])}> <P {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
@ -132,8 +171,11 @@ export const EpisodeLine = ({
seasonNumber, seasonNumber,
releaseDate, releaseDate,
runtime, runtime,
watchedPercent,
watchedStatus,
...props ...props
}: WithLoading<{ }: WithLoading<{
id: string;
slug: string; slug: string;
displayNumber: string; displayNumber: string;
name: string | null; name: string | null;
@ -144,7 +186,8 @@ export const EpisodeLine = ({
seasonNumber: number | null; seasonNumber: number | null;
releaseDate: Date | null; releaseDate: Date | null;
runtime: number | null; runtime: number | null;
id: string; watchedPercent: number | null;
watchedStatus: WatchStatusV | null;
}> & }> &
Stylable) => { Stylable) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -157,18 +200,8 @@ export const EpisodeLine = ({
{ {
alignItems: "center", alignItems: "center",
flexDirection: "row", flexDirection: "row",
child: { fover: {
poster: {
borderColor: "transparent",
borderWidth: px(4),
},
},
focus: {
self: focusReset, self: focusReset,
poster: {
transform: "scale(1.1)" as any,
borderColor: (theme: Theme) => theme.accent,
},
title: { title: {
textDecorationLine: "underline", textDecorationLine: "underline",
}, },
@ -180,16 +213,41 @@ export const EpisodeLine = ({
<P {...css({ width: rem(4), flexShrink: 0, m: ts(1), textAlign: "center" })}> <P {...css({ width: rem(4), flexShrink: 0, m: ts(1), textAlign: "center" })}>
{isLoading ? <Skeleton variant="filltext" /> : displayNumber} {isLoading ? <Skeleton variant="filltext" /> : displayNumber}
</P> </P>
<Image <ImageBackground
src={thumbnail} src={thumbnail}
quality="low" quality="low"
alt="" alt=""
gradient={false}
hideLoad={false}
layout={{ layout={{
width: percent(18), width: percent(18),
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
}} }}
{...(css(["poster", { flexShrink: 0, m: ts(1) }]) as { style: ImageStyle })} {...(css({ flexShrink: 0, m: ts(1) }) as { style: ImageStyle })}
/> >
{(watchedPercent || watchedStatus === WatchStatusV.Completed) && (
<>
<View
{...css({
backgroundColor: (theme) => theme.overlay0,
width: percent(100),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
<View
{...css({
backgroundColor: (theme) => theme.accent,
width: percent(watchedPercent ?? 100),
height: ts(0.5),
position: "absolute",
bottom: 0,
})}
/>
</>
)}
</ImageBackground>
<View {...css({ flexGrow: 1, flexShrink: 1, m: ts(1) })}> <View {...css({ flexGrow: 1, flexShrink: 1, m: ts(1) })}>
<View <View
{...css({ {...css({

View File

@ -497,6 +497,8 @@ export const Header = ({
<EpisodeLine <EpisodeLine
isLoading={false} isLoading={false}
{...(data.watchStatus as ShowWatchStatus).nextEpisode!} {...(data.watchStatus as ShowWatchStatus).nextEpisode!}
watchedPercent={data.watchStatus?.watchedPercent || null}
watchedStatus={data.watchStatus?.status || null}
displayNumber={episodeDisplayNumber((data.watchStatus as ShowWatchStatus).nextEpisode!)!} displayNumber={episodeDisplayNumber((data.watchStatus as ShowWatchStatus).nextEpisode!)!}
/> />
</Container> </Container>

View File

@ -146,6 +146,8 @@ export const EpisodeList = <Props,>({
<EpisodeLine <EpisodeLine
{...item} {...item}
displayNumber={item.isLoading ? undefined! : episodeDisplayNumber(item)!} displayNumber={item.isLoading ? undefined! : episodeDisplayNumber(item)!}
watchedPercent={item.watchStatus?.watchedPercent ?? null}
watchedStatus={item.watchStatus?.status ?? null}
/> />
</> </>
); );
@ -162,6 +164,7 @@ EpisodeList.query = (
path: ["show", slug, "episode"], path: ["show", slug, "episode"],
params: { params: {
seasonNumber: season ? `gte:${season}` : undefined, seasonNumber: season ? `gte:${season}` : undefined,
fields: ["watchStatus"],
}, },
infinite: { infinite: {
value: true, value: true,

View File

@ -18,23 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { import { News, NewsKind, NewsP, QueryIdentifier, getDisplayDate } from "@kyoo/models";
Genre, import { useYoshiki } from "yoshiki/native";
ItemKind,
News,
NewsKind,
NewsP,
QueryIdentifier,
getDisplayDate,
} from "@kyoo/models";
import { H3, IconButton, ts } from "@kyoo/primitives";
import { ReactElement, forwardRef, useRef } from "react";
import { View } from "react-native";
import { px, useYoshiki } from "yoshiki/native";
import { ItemGrid } from "../browse/grid"; import { ItemGrid } from "../browse/grid";
import ChevronLeft from "@material-symbols/svg-400/rounded/chevron_left-fill.svg"; import { InfiniteFetch } from "../fetch-infinite";
import ChevronRight from "@material-symbols/svg-400/rounded/chevron_right-fill.svg";
import { InfiniteFetch, InfiniteFetchList } from "../fetch-infinite";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Header } from "./genre"; import { Header } from "./genre";
import { EpisodeBox, episodeDisplayNumber } from "../details/episode"; import { EpisodeBox, episodeDisplayNumber } from "../details/episode";
@ -74,6 +61,8 @@ export const NewsList = () => {
overview={x.name} overview={x.name}
thumbnail={x.thumbnail} thumbnail={x.thumbnail}
href={x.href} href={x.href}
watchedPercent={x.watchStatus?.watchedPercent || null}
watchedStatus={x.watchStatus?.status || null}
// TODO: support this on mobile too // TODO: support this on mobile too
// @ts-expect-error This is a web only property // @ts-expect-error This is a web only property
{...css({ gridColumnEnd: "span 2" })} {...css({ gridColumnEnd: "span 2" })}
@ -92,6 +81,6 @@ NewsList.query = (): QueryIdentifier<News> => ({
params: { params: {
// Limit the inital numbers of items // Limit the inital numbers of items
limit: 10, limit: 10,
fields: ["show"], fields: ["show", "watchStatus"],
}, },
}); });