Add basic watch status button on details page

This commit is contained in:
Zoe Roux 2023-12-03 14:29:47 +01:00
parent 2f309440cc
commit cfa12b0fed
7 changed files with 147 additions and 22 deletions

View File

@ -29,4 +29,5 @@ export * from "./studio";
export * from "./episode";
export * from "./season";
export * from "./watch-info";
export * from "./watch-status";
export * from "./user";

View File

@ -27,7 +27,7 @@ import { StudioP } from "./studio";
import { BaseEpisodeP } from "./episode.base";
import { CollectionP } from "./collection";
import { MetadataP } from "./metadata";
import { WatchStatusP } from "./watch-status";
import { ShowWatchStatusP, WatchStatusP } from "./watch-status";
/**
* The enum containing show's status.
@ -108,20 +108,7 @@ export const ShowP = withImages(
/**
* Metadata of what an user as started/planned to watch.
*/
watchStatus: WatchStatusP.and(
z.object({
/**
* The number of episodes the user has not seen.
*/
unseenEpisodeCount: z.number().int().gte(0),
/**
* The next episode to watch
*/
nextEpisode: BaseEpisodeP.nullable(),
}),
)
.nullable()
.optional(),
watchStatus: ShowWatchStatusP.nullable().optional(),
}),
"shows",
)

View File

@ -19,7 +19,8 @@
*/
import { z } from "zod";
import { zdate } from "../utils"
import { zdate } from "../utils";
import { BaseEpisodeP } from "./episode.base";
export enum WatchStatusV {
Completed = "Completed",
@ -50,7 +51,20 @@ export const WatchStatusP = z.object({
* Where the player has stopped watching the episode (in percentage between 0 and 100).
* Null if the status is not Watching or if the next episode is not started.
*/
watchedPercent: z.number().int().gte(0).lte(100),
watchedPercent: z.number().int().gte(0).lte(100).nullable(),
});
export type WatchStatus = z.infer<typeof WatchStatusP>;
export const ShowWatchStatusP = WatchStatusP.and(
z.object({
/**
* The number of episodes the user has not seen.
*/
unseenEpisodesCount: z.number().int().gte(0),
/**
* The next episode to watch
*/
nextEpisode: BaseEpisodeP.nullable(),
}),
);
export type ShowWatchStatus = z.infer<typeof ShowWatchStatusP>;

View File

@ -0,0 +1,102 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { IconButton, tooltip } from "@kyoo/primitives";
import { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg";
import Bookmark from "@material-symbols/svg-400/rounded/bookmark-fill.svg";
import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill.svg";
import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { WatchStatusV, queryFn } from "@kyoo/models";
export const WatchListInfo = ({
type,
slug,
status,
...props
}: {
type: "movie" | "show" | "episode";
slug: string;
status: WatchStatusV | null;
color: ComponentProps<typeof IconButton>["color"];
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newStatus: WatchStatusV | null) =>
queryFn({
path: [type, slug, "watchStatus", newStatus && `?status=${newStatus}`],
method: newStatus ? "POST" : "DELETE",
}),
onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }),
});
if (mutation.isPending) status = mutation.variables;
switch (status) {
case null:
return (
<IconButton
icon={BookmarkAdd}
onPress={() => mutation.mutate(WatchStatusV.Planned)}
{...tooltip(t("show.watchlistAdd"))}
{...props}
/>
);
case WatchStatusV.Planned:
return (
<IconButton
icon={Bookmark}
// onPress={() => mutation.mutate(WatchStatusV.)}
{...tooltip(t("show.watchlistEdit"))}
{...props}
/>
);
case WatchStatusV.Watching:
return (
<IconButton
icon={Bookmark}
// onPress={() => mutation.mutate(WatchStatusV.Planned)}
{...tooltip(t("show.watchlistEdit"))}
{...props}
/>
);
case WatchStatusV.Completed:
return (
<IconButton
icon={BookmarkAdded}
onPress={() => mutation.mutate(null)}
{...tooltip(t("show.watchlistRemove"))}
{...props}
/>
);
case WatchStatusV.Droped:
return (
<IconButton
icon={BookmarkRemove}
// onPress={() => mutation.mutate(WatchStatusV.Planned)}
{...tooltip(t("show.watchlistEdit"))}
{...props}
/>
);
}
};

View File

@ -50,7 +50,7 @@ import {
} from "@kyoo/primitives";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { ImageStyle, Platform, Text, View } from "react-native";
import { ImageStyle, Platform, View } from "react-native";
import {
Theme,
md,
@ -69,7 +69,8 @@ import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
import { Rating } from "../components/rating";
import { displayRuntime } from "./episode";
import { PartOf } from "./collection";
import { WatchListInfo } from "../components/watchlist-info";
import { WatchStatusV } from "@kyoo/models/src/resources/watch-status";
export const TitleLine = ({
isLoading,
@ -83,6 +84,8 @@ export const TitleLine = ({
studio,
trailerUrl,
type,
watchStatus,
slug,
...props
}: {
isLoading: boolean;
@ -95,7 +98,9 @@ export const TitleLine = ({
poster?: KyooImage | null;
studio?: Studio | null;
trailerUrl?: string | null;
watchStatus?: WatchStatusV | null;
type: "movie" | "show" | "collection";
slug?: string;
} & Stylable) => {
const { css, theme } = useYoshiki();
const { t } = useTranslation();
@ -223,6 +228,14 @@ export const TitleLine = ({
{...tooltip(t("show.trailer"))}
/>
)}
{watchStatus !== undefined && type !== "collection" && slug && (
<WatchListInfo
type={type}
slug={slug}
status={watchStatus}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
/>
)}
{rating !== null && (
<>
<DottedSeparator
@ -405,6 +418,7 @@ export const Header = ({
<TitleLine
isLoading={isLoading}
type={type}
slug={data?.slug}
playHref={data?.playHref}
name={data?.name}
tagline={data?.tagline}
@ -414,6 +428,7 @@ export const Header = ({
poster={data?.poster}
trailerUrl={data?.trailer}
studio={data?.studio}
watchStatus={data?.watchStatus?.status ?? null}
{...css(Header.childStyle)}
/>
</ImageBackground>

View File

@ -19,7 +19,10 @@
"tags": "Tags",
"links": "Links",
"jumpToSeason": "Jump to season",
"partOf": "Part of the"
"partOf": "Part of the",
"watchlistAdd": "Add to your plan to watch list",
"watchlistEdit": "Edit watch status",
"watchlistRemove": "Mark as not seen"
},
"browse": {
"sortby": "Sort by {{key}}",

View File

@ -19,7 +19,10 @@
"tags": "Tags",
"links": "Liens",
"jumpToSeason": "Aller sur une saison",
"partOf": "Fait parti de"
"partOf": "Fait parti de",
"watchlistAdd": "Ajouter à votre liste de visionnage",
"watchlistEdit": "Editer le status",
"watchlistRemove": "Marquer comme non vu"
},
"browse": {
"sortby": "Trier par {{key}}",