mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Add watch percent display on episodes
This commit is contained in:
parent
fe155898bb
commit
e70174cb24
@ -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";
|
||||||
|
|
||||||
|
@ -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;
|
||||||
})}
|
})}
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user