Clean up kind handling in the front

This commit is contained in:
Zoe Roux 2023-12-18 15:03:04 +01:00
parent 7b035411c0
commit 2e0a0e5eb0
25 changed files with 78 additions and 117 deletions

View File

@ -17,7 +17,6 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;

View File

@ -18,7 +18,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;

View File

@ -25,6 +25,7 @@ using Kyoo.Abstractions.Models.Attributes;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using static System.Text.Json.JsonNamingPolicy;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
@ -99,7 +100,7 @@ namespace Kyoo.Core.Api
PropertyName = "kind", PropertyName = "kind",
UnderlyingName = "kind", UnderlyingName = "kind",
PropertyType = typeof(string), PropertyType = typeof(string),
ValueProvider = new FixedValueProvider(type.Name), ValueProvider = new FixedValueProvider(CamelCase.ConvertName(type.Name)),
Readable = true, Readable = true,
Writable = false, Writable = false,
TypeNameHandling = TypeNameHandling.None, TypeNameHandling = TypeNameHandling.None,

View File

@ -19,7 +19,7 @@
*/ */
import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react"; import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
import { UserP } from "./resources"; import { User, UserP } from "./resources";
import { z } from "zod"; import { z } from "zod";
import { zdate } from "./utils"; import { zdate } from "./utils";
import { removeAccounts, setAccountCookie, updateAccount } from "./account-internal"; import { removeAccounts, setAccountCookie, updateAccount } from "./account-internal";
@ -38,13 +38,13 @@ export const TokenP = z.object({
}); });
export type Token = z.infer<typeof TokenP>; export type Token = z.infer<typeof TokenP>;
export const AccountP = UserP.and( export const AccountP = UserP.merge(z.object({
z.object({ // set it optional for accounts logged in before the kind was present
token: TokenP, kind: z.literal("user").optional(),
apiUrl: z.string(), token: TokenP,
selected: z.boolean(), apiUrl: z.string(),
}), selected: z.boolean(),
); }));
export type Account = z.infer<typeof AccountP>; export type Account = z.infer<typeof AccountP>;
const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]); const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]);
@ -100,7 +100,7 @@ export const AccountProvider = ({
const user = useFetch({ const user = useFetch({
path: ["auth", "me"], path: ["auth", "me"],
parser: UserP, parser: UserP,
placeholderData: selected, placeholderData: selected as User,
enabled: !!selected, enabled: !!selected,
timeout: 5_000, timeout: 5_000,
}); });

View File

@ -22,7 +22,7 @@ import { z } from "zod";
import { withImages, ResourceP } from "../traits"; import { withImages, ResourceP } from "../traits";
export const CollectionP = withImages( export const CollectionP = withImages(
ResourceP.extend({ ResourceP("collection").extend({
/** /**
* The title of this collection. * The title of this collection.
*/ */
@ -31,8 +31,7 @@ export const CollectionP = withImages(
* The summary of this show. * The summary of this show.
*/ */
overview: z.string().nullable(), overview: z.string().nullable(),
}), })
"collections",
).transform((x) => ({ ).transform((x) => ({
...x, ...x,
href: `/collection/${x.slug}`, href: `/collection/${x.slug}`,

View File

@ -24,7 +24,7 @@ import { withImages, imageFn } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const BaseEpisodeP = withImages( export const BaseEpisodeP = withImages(
ResourceP.extend({ ResourceP("episode").extend({
/** /**
* The season in witch this episode is in. * The season in witch this episode is in.
*/ */
@ -72,7 +72,6 @@ export const BaseEpisodeP = withImages(
*/ */
showId: z.string(), showId: z.string(),
}), }),
"episodes",
) )
.transform((x) => ({ .transform((x) => ({
...x, ...x,

View File

@ -23,28 +23,19 @@ import { CollectionP } from "./collection";
import { MovieP } from "./movie"; import { MovieP } from "./movie";
import { ShowP } from "./show"; import { ShowP } from "./show";
/**
* The type of item, ether a show, a movie or a collection.
*/
export enum ItemKind {
Show = "Show",
Movie = "Movie",
Collection = "Collection",
}
export const LibraryItemP = z.union([ export const LibraryItemP = z.union([
/* /*
* Either a Show * Either a Show
*/ */
ShowP.and(z.object({ kind: z.literal(ItemKind.Show) })), ShowP,
/* /*
* Or a Movie * Or a Movie
*/ */
MovieP.and(z.object({ kind: z.literal(ItemKind.Movie) })), MovieP,
/* /*
* Or a Collection * Or a Collection
*/ */
CollectionP.and(z.object({ kind: z.literal(ItemKind.Collection) })), CollectionP,
]); ]);
/** /**

View File

@ -29,7 +29,7 @@ import { MetadataP } from "./metadata";
import { WatchStatusP } from "./watch-status"; import { WatchStatusP } from "./watch-status";
export const MovieP = withImages( export const MovieP = withImages(
ResourceP.extend({ ResourceP("movie").extend({
/** /**
* The title of this movie. * The title of this movie.
*/ */
@ -105,7 +105,6 @@ export const MovieP = withImages(
*/ */
watchStatus: WatchStatusP.optional().nullable(), watchStatus: WatchStatusP.optional().nullable(),
}), }),
"movies",
) )
.transform((x) => ({ .transform((x) => ({
...x, ...x,

View File

@ -22,23 +22,15 @@ import { z } from "zod";
import { MovieP } from "./movie"; import { MovieP } from "./movie";
import { EpisodeP } from "./episode"; import { EpisodeP } from "./episode";
/**
* The type of item, ether a a movie or an episode.
*/
export enum NewsKind {
Episode = "Episode",
Movie = "Movie",
}
export const NewsP = z.union([ export const NewsP = z.union([
/* /*
* Either an episode * Either an episode
*/ */
EpisodeP.and(z.object({ kind: z.literal(NewsKind.Episode) })), EpisodeP,
/* /*
* Or a Movie * Or a Movie
*/ */
MovieP.and(z.object({ kind: z.literal(NewsKind.Movie) })), MovieP,
]); ]);
/** /**

View File

@ -23,7 +23,7 @@ import { withImages } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const PersonP = withImages( export const PersonP = withImages(
ResourceP.extend({ ResourceP("people").extend({
/** /**
* The name of this person. * The name of this person.
*/ */
@ -40,7 +40,6 @@ export const PersonP = withImages(
*/ */
role: z.string().optional(), role: z.string().optional(),
}), }),
"people",
); );
/** /**

View File

@ -24,7 +24,7 @@ import { withImages } from "../traits";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const SeasonP = withImages( export const SeasonP = withImages(
ResourceP.extend({ ResourceP("season").extend({
/** /**
* The name of this season. * The name of this season.
*/ */
@ -50,7 +50,6 @@ export const SeasonP = withImages(
*/ */
episodesCount: z.number(), episodesCount: z.number(),
}), }),
"seasons",
); );
/** /**

View File

@ -22,10 +22,8 @@ import { z } from "zod";
import { zdate } from "../utils"; import { zdate } from "../utils";
import { withImages, ResourceP } from "../traits"; import { withImages, ResourceP } from "../traits";
import { Genre } from "./genre"; import { Genre } from "./genre";
import { SeasonP } from "./season";
import { StudioP } from "./studio"; import { StudioP } from "./studio";
import { BaseEpisodeP } from "./episode.base"; import { BaseEpisodeP } from "./episode.base";
import { CollectionP } from "./collection";
import { MetadataP } from "./metadata"; import { MetadataP } from "./metadata";
import { ShowWatchStatusP } from "./watch-status"; import { ShowWatchStatusP } from "./watch-status";
@ -40,7 +38,7 @@ export enum Status {
} }
export const ShowP = withImages( export const ShowP = withImages(
ResourceP.extend({ ResourceP("show").extend({
/** /**
* The title of this show. * The title of this show.
*/ */
@ -106,7 +104,6 @@ export const ShowP = withImages(
*/ */
episodesCount: z.number().int().gte(0).optional(), episodesCount: z.number().int().gte(0).optional(),
}), }),
"shows",
) )
.transform((x) => { .transform((x) => {
if (!x.thumbnail && x.poster) { if (!x.thumbnail && x.poster) {

View File

@ -21,7 +21,7 @@
import { z } from "zod"; import { z } from "zod";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
export const StudioP = ResourceP.extend({ export const StudioP = ResourceP("studio").extend({
/** /**
* The name of this studio. * The name of this studio.
*/ */

View File

@ -21,10 +21,7 @@
import { z } from "zod"; import { z } from "zod";
import { ResourceP } from "../traits/resource"; import { ResourceP } from "../traits/resource";
/** export const UserP = ResourceP("user").extend({
* The library that will contain Shows, Collections...
*/
export const UserP = ResourceP.extend({
/** /**
* The name of this user. * The name of this user.
*/ */

View File

@ -22,23 +22,15 @@ import { z } from "zod";
import { MovieP } from "./movie"; import { MovieP } from "./movie";
import { ShowP } from "./show"; import { ShowP } from "./show";
/**
* The type of item, ether a show, a movie or an episode.
*/
export enum WatchlistKind {
Show = "Show",
Movie = "Movie",
}
export const WatchlistP = z.union([ export const WatchlistP = z.union([
/* /*
* Either a show * Either a show
*/ */
ShowP.and(z.object({ kind: z.literal(WatchlistKind.Show) })), ShowP,
/* /*
* Or a Movie * Or a Movie
*/ */
MovieP.and(z.object({ kind: z.literal(WatchlistKind.Movie) })), MovieP,
]); ]);
/** /**

View File

@ -62,13 +62,13 @@ const addQualities = (x: object | null | undefined, href: string) => {
}; };
}; };
export const withImages = <T extends ZodRawShape>(parser: ZodObject<T>, type: string) => { export const withImages = <T extends ZodRawShape>(parser: ZodObject<T>) => {
return parser.merge(ImagesP).transform((x) => { return parser.merge(ImagesP).transform((x) => {
return { return {
...x, ...x,
poster: addQualities(x.poster, `/${type}/${x.slug}/poster`), poster: addQualities(x.poster, `/${x.kind}/${x.slug}/poster`),
thumbnail: addQualities(x.thumbnail, `/${type}/${x.slug}/thumbnail`), thumbnail: addQualities(x.thumbnail, `/${x.kind}/${x.slug}/thumbnail`),
logo: addQualities(x.logo, `/${type}/${x.slug}/logo`), logo: addQualities(x.logo, `/${x.kind}/${x.slug}/logo`),
}; };
}); });
}; };

View File

@ -20,20 +20,26 @@
import { z } from "zod"; import { z } from "zod";
export const ResourceP = z.object({ export const ResourceP = <T extends string>(kind: T) =>
/** z.object({
* A unique ID for this type of resource. This can't be changed and duplicates are not allowed. /**
*/ * A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
id: z.string(), */
id: z.string(),
/** /**
* A human-readable identifier that can be used instead of an ID. A slug must be unique for a type * A human-readable identifier that can be used instead of an ID. A slug must be unique for a type
* of resource but it can be changed. * of resource but it can be changed.
*/ */
slug: z.string(), slug: z.string(),
});
/**
* The type of resource
*/
kind: z.literal(kind),
});
/** /**
* The base trait used to represent identifiable resources. * The base trait used to represent identifiable resources.
*/ */
export type Resource = z.infer<typeof ResourceP>; export type Resource = z.infer<ReturnType<typeof ResourceP>>;

View File

@ -71,7 +71,7 @@ export const ImageBackground = <AsProps = ViewProps,>({
}: { }: {
as?: ComponentType<AsProps>; as?: ComponentType<AsProps>;
gradient?: Partial<LinearGradientProps> | boolean; gradient?: Partial<LinearGradientProps> | boolean;
children: ReactNode; children?: ReactNode;
containerStyle?: YoshikiEnhanced<ViewStyle>; containerStyle?: YoshikiEnhanced<ViewStyle>;
imageStyle?: YoshikiEnhanced<ImageStyle>; imageStyle?: YoshikiEnhanced<ImageStyle>;
hideLoad?: boolean; hideLoad?: boolean;

View File

@ -23,7 +23,6 @@ import {
QueryPage, QueryPage,
LibraryItem, LibraryItem,
LibraryItemP, LibraryItemP,
ItemKind,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { ComponentProps, useState } from "react"; import { ComponentProps, useState } from "react";
@ -47,16 +46,16 @@ export const itemMap = (
isLoading: item.isLoading, isLoading: item.isLoading,
slug: item.slug, slug: item.slug,
name: item.name, name: item.name,
subtitle: item.kind !== ItemKind.Collection ? getDisplayDate(item) : undefined, subtitle: item.kind !== "collection" ? getDisplayDate(item) : undefined,
href: item.href, href: item.href,
poster: item.poster, poster: item.poster,
thumbnail: item.thumbnail, thumbnail: item.thumbnail,
watchStatus: item.kind !== ItemKind.Collection ? item.watchStatus?.status ?? null : null, watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null,
type: item.kind.toLowerCase() as any, type: item.kind,
watchPercent: watchPercent:
item.kind !== ItemKind.Collection ? item.watchStatus?.watchedPercent ?? null : null, item.kind !== "collection" ? item.watchStatus?.watchedPercent ?? null : null,
unseenEpisodesCount: unseenEpisodesCount:
item.kind === ItemKind.Show item.kind === "show"
? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount! ? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!
: null, : null,
}; };

View File

@ -21,7 +21,6 @@
import { import {
Collection, Collection,
CollectionP, CollectionP,
ItemKind,
LibraryItem, LibraryItem,
LibraryItemP, LibraryItemP,
QueryIdentifier, QueryIdentifier,
@ -161,20 +160,20 @@ export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => {
<ItemDetails <ItemDetails
isLoading={x.isLoading as any} isLoading={x.isLoading as any}
slug={x.slug} slug={x.slug}
type={x.kind?.toLowerCase() as any} type={x.kind}
name={x.name} name={x.name}
tagline={"tagline" in x ? x.tagline : null} tagline={"tagline" in x ? x.tagline : null}
overview={x.overview} overview={x.overview}
poster={x.poster} poster={x.poster}
subtitle={x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined} subtitle={x.kind !== "collection" && !x.isLoading ? getDisplayDate(x) : undefined}
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 !== "collection" && !x.isLoading ? x.playHref : undefined}
watchStatus={ watchStatus={
!x.isLoading && x.kind !== ItemKind.Collection ? x.watchStatus?.status ?? null : null !x.isLoading && x.kind !== "collection" ? x.watchStatus?.status ?? null : null
} }
unseenEpisodesCount={ unseenEpisodesCount={
x.kind === ItemKind.Show ? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount! : null x.kind === "show" ? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount! : null
} }
{...css({ marginX: ItemGrid.layout.gap })} {...css({ marginX: ItemGrid.layout.gap })}
/> />

View File

@ -37,7 +37,7 @@ import { percent, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { KyooImage, WatchStatusV } from "@kyoo/models"; import { KyooImage, WatchStatusV } from "@kyoo/models";
import { ItemProgress } from "../browse/grid"; import { ItemProgress } from "../browse/grid";
import { EpisodesContext } from "../components/context-menus"; import { EpisodesContext } from "../components/context-menus";
import { useRef, useState } from "react"; import { useState } from "react";
export const episodeDisplayNumber = ( export const episodeDisplayNumber = (
episode: { episode: {

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Genre, ItemKind, QueryPage } from "@kyoo/models"; import { Genre, QueryPage } from "@kyoo/models";
import { Fetch } from "../fetch"; import { Fetch } from "../fetch";
import { Header } from "./header"; import { Header } from "./header";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
@ -40,7 +40,7 @@ export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => {
tagline={"tagline" in x ? x.tagline : null} tagline={"tagline" in x ? x.tagline : null}
overview={x.overview} overview={x.overview}
thumbnail={x.thumbnail} thumbnail={x.thumbnail}
link={x.kind !== ItemKind.Collection && !x.isLoading ? x.playHref : undefined} link={x.kind !== "collection" && !x.isLoading ? x.playHref : undefined}
infoLink={x.href} infoLink={x.href}
/> />
)} )}

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { News, NewsKind, NewsP, QueryIdentifier, getDisplayDate } from "@kyoo/models"; import { News, NewsP, QueryIdentifier, getDisplayDate } from "@kyoo/models";
import { ItemGrid } from "../browse/grid"; import { ItemGrid } from "../browse/grid";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -37,13 +37,13 @@ export const NewsList = () => {
query={NewsList.query()} query={NewsList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }} layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) => getItemType={(x, i) =>
x.kind === NewsKind.Movie || (x.isLoading && i % 2) ? "movie" : "episode" x.kind === "movie" || (x.isLoading && i % 2) ? "movie" : "episode"
} }
getItemSize={(kind) => (kind === "episode" ? 2 : 1)} getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
empty={t("home.none")} empty={t("home.none")}
> >
{(x, i) => {(x, i) =>
x.kind === NewsKind.Movie || (x.isLoading && i % 2) ? ( x.kind === "movie" || (x.isLoading && i % 2) ? (
<ItemGrid <ItemGrid
isLoading={x.isLoading as any} isLoading={x.isLoading as any}
href={x.href} href={x.href}
@ -58,9 +58,9 @@ export const NewsList = () => {
<EpisodeBox <EpisodeBox
isLoading={x.isLoading as any} isLoading={x.isLoading as any}
slug={x.slug} slug={x.slug}
showSlug={x.kind === NewsKind.Episode ? x.showId : null} showSlug={x.kind === "episode" ? x.showId : null}
name={ name={
x.kind === NewsKind.Episode x.kind === "episode"
? `${x.show!.name} ${episodeDisplayNumber(x)}` ? `${x.show!.name} ${episodeDisplayNumber(x)}`
: undefined : undefined
} }

View File

@ -20,7 +20,6 @@
import { import {
Genre, Genre,
ItemKind,
KyooImage, KyooImage,
LibraryItem, LibraryItem,
LibraryItemP, LibraryItemP,
@ -258,24 +257,20 @@ export const Recommanded = () => {
<ItemDetails <ItemDetails
isLoading={x.isLoading as any} isLoading={x.isLoading as any}
slug={x.slug} slug={x.slug}
type={x.kind?.toLowerCase() as any} type={x.kind}
name={x.name} name={x.name}
tagline={"tagline" in x ? x.tagline : null} tagline={"tagline" in x ? x.tagline : null}
overview={x.overview} overview={x.overview}
poster={x.poster} poster={x.poster}
subtitle={ subtitle={x.kind !== "collection" && !x.isLoading ? getDisplayDate(x) : undefined}
x.kind !== ItemKind.Collection && !x.isLoading ? getDisplayDate(x) : undefined
}
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 !== "collection" && !x.isLoading ? x.playHref : undefined}
watchStatus={ watchStatus={
!x.isLoading && x.kind !== ItemKind.Collection ? x.watchStatus?.status ?? null : null !x.isLoading && x.kind !== "collection" ? x.watchStatus?.status ?? null : null
} }
unseenEpisodesCount={ unseenEpisodesCount={
x.kind === ItemKind.Show x.kind === "show" ? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount! : null
? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount!
: null
} }
/> />
)} )}

View File

@ -21,7 +21,6 @@
import { import {
QueryIdentifier, QueryIdentifier,
Watchlist, Watchlist,
WatchlistKind,
WatchlistP, WatchlistP,
getDisplayDate, getDisplayDate,
useAccount, useAccount,
@ -48,7 +47,7 @@ export const WatchlistList = () => {
query={WatchlistList.query()} query={WatchlistList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }} layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) => getItemType={(x, i) =>
(x.kind === WatchlistKind.Show && x.watchStatus?.nextEpisode) || (x.isLoading && i % 2) (x.kind === "show" && x.watchStatus?.nextEpisode) || (x.isLoading && i % 2)
? "episode" ? "episode"
: "item" : "item"
} }
@ -56,8 +55,8 @@ export const WatchlistList = () => {
empty={t("home.none")} empty={t("home.none")}
> >
{(x, i) => { {(x, i) => {
const episode = x.kind === WatchlistKind.Show ? x.watchStatus?.nextEpisode : null; const episode = x.kind === "show" ? x.watchStatus?.nextEpisode : null;
return (x.kind === WatchlistKind.Show && x.watchStatus?.nextEpisode) || return (x.kind === "show" && x.watchStatus?.nextEpisode) ||
(x.isLoading && i % 2) ? ( (x.isLoading && i % 2) ? (
<EpisodeBox <EpisodeBox
isLoading={x.isLoading as any} isLoading={x.isLoading as any}
@ -83,9 +82,9 @@ export const WatchlistList = () => {
watchStatus={x.watchStatus?.status || null} watchStatus={x.watchStatus?.status || null}
watchPercent={x.watchStatus?.watchedPercent || null} watchPercent={x.watchStatus?.watchedPercent || null}
unseenEpisodesCount={ unseenEpisodesCount={
x.kind === WatchlistKind.Show ? x.watchStatus?.unseenEpisodesCount : null x.kind === "show" ? x.watchStatus?.unseenEpisodesCount : null
} }
type={x.kind === WatchlistKind.Movie ? "movie" : "show"} type={x.kind}
/> />
); );
}} }}