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/>.
*/
import { useLayoutEffect, useState } from "react";
import { useState } from "react";
import { ImageStyle, View, ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { BlurhashContainer, blurHashToDataURL } from "./blurhash.web";
import { BlurhashContainer } from "./blurhash.web";
import { Skeleton } from "../skeleton";
import NextImage from "next/image";

View File

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

View File

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

View File

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

View File

@ -146,6 +146,8 @@ export const EpisodeList = <Props,>({
<EpisodeLine
{...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"],
params: {
seasonNumber: season ? `gte:${season}` : undefined,
fields: ["watchStatus"],
},
infinite: {
value: true,

View File

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