mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add watch status indicator on items list and recommanded card
This commit is contained in:
parent
e5dde472c9
commit
c24c8914bf
@ -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>
|
||||
)}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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"],
|
||||
},
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user