mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-30 19:54:16 -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 {
|
import {
|
||||||
Link,
|
Link,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Poster,
|
|
||||||
ts,
|
ts,
|
||||||
focusReset,
|
focusReset,
|
||||||
P,
|
P,
|
||||||
@ -31,7 +30,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { ImageStyle, View } from "react-native";
|
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 { Layout, WithLoading } from "../fetch";
|
||||||
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
||||||
|
|
||||||
@ -41,14 +40,16 @@ export const ItemGrid = ({
|
|||||||
subtitle,
|
subtitle,
|
||||||
poster,
|
poster,
|
||||||
isLoading,
|
isLoading,
|
||||||
watchInfo,
|
watchStatus,
|
||||||
|
unseenEpisodesCount,
|
||||||
...props
|
...props
|
||||||
}: WithLoading<{
|
}: WithLoading<{
|
||||||
href: string;
|
href: string;
|
||||||
name: string;
|
name: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
poster?: KyooImage | null;
|
poster?: KyooImage | null;
|
||||||
watchInfo: WatchStatusV | string | null;
|
watchStatus: WatchStatusV | null;
|
||||||
|
unseenEpisodesCount: number | null;
|
||||||
}> &
|
}> &
|
||||||
Stylable<"text">) => {
|
Stylable<"text">) => {
|
||||||
const { css } = useYoshiki("grid");
|
const { css } = useYoshiki("grid");
|
||||||
@ -89,7 +90,7 @@ export const ItemGrid = ({
|
|||||||
layout={{ width: percent(100) }}
|
layout={{ width: percent(100) }}
|
||||||
{...(css("poster") as { style: ImageStyle })}
|
{...(css("poster") as { style: ImageStyle })}
|
||||||
>
|
>
|
||||||
{watchInfo && (
|
{(watchStatus === WatchStatusV.Completed || unseenEpisodesCount) && (
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@ -104,10 +105,10 @@ export const ItemGrid = ({
|
|||||||
borderRadius: 999999,
|
borderRadius: 999999,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{watchInfo === WatchStatusV.Completed ? (
|
{watchStatus === WatchStatusV.Completed ? (
|
||||||
<Icon icon={Done} size={16} />
|
<Icon icon={Done} size={16} />
|
||||||
) : (
|
) : (
|
||||||
<P {...css({ m: 0, textAlign: "center" })}>{watchInfo}</P>
|
<P {...css({ m: 0, textAlign: "center" })}>{unseenEpisodesCount}</P>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
@ -25,7 +25,6 @@ import {
|
|||||||
LibraryItemP,
|
LibraryItemP,
|
||||||
ItemKind,
|
ItemKind,
|
||||||
getDisplayDate,
|
getDisplayDate,
|
||||||
WatchStatusV,
|
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
import { ComponentProps, useState } from "react";
|
import { ComponentProps, useState } from "react";
|
||||||
import { createParam } from "solito";
|
import { createParam } from "solito";
|
||||||
@ -42,16 +41,7 @@ const { useParam } = createParam<{ sortBy?: string }>();
|
|||||||
export const itemMap = (
|
export const itemMap = (
|
||||||
item: WithLoading<LibraryItem>,
|
item: WithLoading<LibraryItem>,
|
||||||
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
||||||
if (item.isLoading) return item;
|
if (item.isLoading) return item as any;
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
isLoading: item.isLoading,
|
isLoading: item.isLoading,
|
||||||
@ -60,7 +50,11 @@ export const itemMap = (
|
|||||||
href: item.href,
|
href: item.href,
|
||||||
poster: item.poster,
|
poster: item.poster,
|
||||||
thumbnail: item.thumbnail,
|
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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KyooImage } from "@kyoo/models";
|
import { KyooImage, WatchStatusV } from "@kyoo/models";
|
||||||
import { Link, P, Skeleton, ts, ImageBackground, Poster, Heading } from "@kyoo/primitives";
|
import {
|
||||||
|
Link,
|
||||||
|
P,
|
||||||
|
Skeleton,
|
||||||
|
ts,
|
||||||
|
ImageBackground,
|
||||||
|
Poster,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
PosterBackground,
|
||||||
|
} from "@kyoo/primitives";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||||
import { Layout, WithLoading } from "../fetch";
|
import { Layout, WithLoading } from "../fetch";
|
||||||
|
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
||||||
|
|
||||||
export const ItemList = ({
|
export const ItemList = ({
|
||||||
href,
|
href,
|
||||||
@ -32,12 +43,17 @@ export const ItemList = ({
|
|||||||
thumbnail,
|
thumbnail,
|
||||||
poster,
|
poster,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
watchStatus,
|
||||||
|
unseenEpisodesCount,
|
||||||
|
...props
|
||||||
}: WithLoading<{
|
}: WithLoading<{
|
||||||
href: string;
|
href: string;
|
||||||
name: string;
|
name: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
poster?: KyooImage | null;
|
poster?: KyooImage | null;
|
||||||
thumbnail?: KyooImage | null;
|
thumbnail?: KyooImage | null;
|
||||||
|
watchStatus: WatchStatusV | null;
|
||||||
|
unseenEpisodesCount: number | null;
|
||||||
}>) => {
|
}>) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const [isHovered, setHovered] = useState(0);
|
const [isHovered, setHovered] = useState(0);
|
||||||
@ -59,14 +75,17 @@ export const ItemList = ({
|
|||||||
imageStyle={{
|
imageStyle={{
|
||||||
borderRadius: px(6),
|
borderRadius: px(6),
|
||||||
}}
|
}}
|
||||||
{...css({
|
{...css(
|
||||||
alignItems: "center",
|
{
|
||||||
justifyContent: "space-evenly",
|
alignItems: "center",
|
||||||
flexDirection: "row",
|
justifyContent: "space-evenly",
|
||||||
height: ItemList.layout.size,
|
flexDirection: "row",
|
||||||
borderRadius: px(6),
|
height: ItemList.layout.size,
|
||||||
marginX: ItemList.layout.gap,
|
borderRadius: px(6),
|
||||||
})}
|
marginX: ItemList.layout.gap,
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
@ -104,13 +123,36 @@ export const ItemList = ({
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Poster
|
<PosterBackground
|
||||||
src={poster}
|
src={poster}
|
||||||
alt=""
|
alt=""
|
||||||
quality="low"
|
quality="low"
|
||||||
forcedLoading={isLoading}
|
forcedLoading={isLoading}
|
||||||
layout={{ height: percent(80) }}
|
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>
|
</ImageBackground>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -25,15 +25,17 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
LibraryItemP,
|
LibraryItemP,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
|
WatchStatusV,
|
||||||
getDisplayDate,
|
getDisplayDate,
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
import {
|
import {
|
||||||
Chip,
|
Chip,
|
||||||
H3,
|
H3,
|
||||||
|
Icon,
|
||||||
IconFab,
|
IconFab,
|
||||||
ImageBackground,
|
|
||||||
Link,
|
Link,
|
||||||
P,
|
P,
|
||||||
|
PosterBackground,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
SubP,
|
SubP,
|
||||||
focusReset,
|
focusReset,
|
||||||
@ -48,6 +50,7 @@ import { Layout, WithLoading } from "../fetch";
|
|||||||
import { InfiniteFetch } from "../fetch-infinite";
|
import { InfiniteFetch } from "../fetch-infinite";
|
||||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||||
import { ItemGrid } from "../browse/grid";
|
import { ItemGrid } from "../browse/grid";
|
||||||
|
import Done from "@material-symbols/svg-400/rounded/done-fill.svg";
|
||||||
|
|
||||||
export const ItemDetails = ({
|
export const ItemDetails = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -59,6 +62,8 @@ export const ItemDetails = ({
|
|||||||
genres,
|
genres,
|
||||||
href,
|
href,
|
||||||
playHref,
|
playHref,
|
||||||
|
watchStatus,
|
||||||
|
unseenEpisodesCount,
|
||||||
...props
|
...props
|
||||||
}: WithLoading<{
|
}: WithLoading<{
|
||||||
name: string;
|
name: string;
|
||||||
@ -69,6 +74,8 @@ export const ItemDetails = ({
|
|||||||
overview: string | null;
|
overview: string | null;
|
||||||
href: string;
|
href: string;
|
||||||
playHref: string | null;
|
playHref: string | null;
|
||||||
|
watchStatus: WatchStatusV | null;
|
||||||
|
unseenEpisodesCount: number | null;
|
||||||
}>) => {
|
}>) => {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -100,13 +107,12 @@ export const ItemDetails = ({
|
|||||||
props,
|
props,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ImageBackground
|
<PosterBackground
|
||||||
src={poster}
|
src={poster}
|
||||||
alt=""
|
alt=""
|
||||||
quality="low"
|
quality="low"
|
||||||
gradient={false}
|
|
||||||
forcedLoading={isLoading}
|
forcedLoading={isLoading}
|
||||||
{...css({ height: percent(100), aspectRatio: 2 / 3 })}
|
layout={{ height: percent(100) }}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
@ -127,7 +133,29 @@ export const ItemDetails = ({
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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" })}>
|
<View {...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end" })}>
|
||||||
{(isLoading || tagline) && (
|
{(isLoading || tagline) && (
|
||||||
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
<Skeleton {...css({ m: ts(1), marginVertical: ts(2) })}>
|
||||||
@ -195,7 +223,7 @@ export const Recommanded = () => {
|
|||||||
fetchMore={false}
|
fetchMore={false}
|
||||||
{...css({ padding: 0 })}
|
{...css({ padding: 0 })}
|
||||||
>
|
>
|
||||||
{(x, i) => (
|
{(x) => (
|
||||||
<ItemDetails
|
<ItemDetails
|
||||||
isLoading={x.isLoading as any}
|
isLoading={x.isLoading as any}
|
||||||
name={x.name}
|
name={x.name}
|
||||||
@ -208,6 +236,14 @@ export const Recommanded = () => {
|
|||||||
genres={"genres" in x ? x.genres : null}
|
genres={"genres" in x ? x.genres : null}
|
||||||
href={x.href}
|
href={x.href}
|
||||||
playHref={x.kind !== ItemKind.Collection && !x.isLoading ? x.playHref : undefined}
|
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>
|
</InfiniteFetch>
|
||||||
@ -222,6 +258,6 @@ Recommanded.query = (): QueryIdentifier<LibraryItem> => ({
|
|||||||
params: {
|
params: {
|
||||||
sortBy: "random",
|
sortBy: "random",
|
||||||
limit: 6,
|
limit: 6,
|
||||||
fields: ["firstEpisode"],
|
fields: ["firstEpisode", "episodesCount", "watchStatus"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ import { InfiniteFetch } from "../fetch-infinite";
|
|||||||
import { ItemList } from "../browse/list";
|
import { ItemList } from "../browse/list";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ItemGrid } from "../browse/grid";
|
import { ItemGrid } from "../browse/grid";
|
||||||
|
import { itemMap } from "../browse";
|
||||||
|
|
||||||
export const VerticalRecommanded = () => {
|
export const VerticalRecommanded = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -40,19 +41,7 @@ export const VerticalRecommanded = () => {
|
|||||||
layout={{ ...ItemList.layout, layout: "vertical" }}
|
layout={{ ...ItemList.layout, layout: "vertical" }}
|
||||||
fetchMore={false}
|
fetchMore={false}
|
||||||
>
|
>
|
||||||
{(x, i) => (
|
{(x, i) => <ItemList key={x.id ?? i} {...itemMap(x)} />}
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfiniteFetch>
|
</InfiniteFetch>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -63,6 +52,7 @@ VerticalRecommanded.query = (): QueryIdentifier<LibraryItem> => ({
|
|||||||
infinite: true,
|
infinite: true,
|
||||||
path: ["items"],
|
path: ["items"],
|
||||||
params: {
|
params: {
|
||||||
|
fields: ["episodesCount", "watchStatus"],
|
||||||
sortBy: "random",
|
sortBy: "random",
|
||||||
limit: 3,
|
limit: 3,
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user