diff --git a/front/packages/models/src/resources/index.ts b/front/packages/models/src/resources/index.ts index 310781fb..8f5b9ce7 100644 --- a/front/packages/models/src/resources/index.ts +++ b/front/packages/models/src/resources/index.ts @@ -29,4 +29,5 @@ export * from "./studio"; export * from "./episode"; export * from "./season"; export * from "./watch-info"; +export * from "./watch-status"; export * from "./user"; diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index b3943fff..2708d197 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -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", ) diff --git a/front/packages/models/src/resources/watch-status.ts b/front/packages/models/src/resources/watch-status.ts index 990ac02a..43dff66d 100644 --- a/front/packages/models/src/resources/watch-status.ts +++ b/front/packages/models/src/resources/watch-status.ts @@ -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; + +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; diff --git a/front/packages/ui/src/components/watchlist-info.tsx b/front/packages/ui/src/components/watchlist-info.tsx new file mode 100644 index 00000000..65c17fb8 --- /dev/null +++ b/front/packages/ui/src/components/watchlist-info.tsx @@ -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 . + */ + +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["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 ( + mutation.mutate(WatchStatusV.Planned)} + {...tooltip(t("show.watchlistAdd"))} + {...props} + /> + ); + case WatchStatusV.Planned: + return ( + mutation.mutate(WatchStatusV.)} + {...tooltip(t("show.watchlistEdit"))} + {...props} + /> + ); + case WatchStatusV.Watching: + return ( + mutation.mutate(WatchStatusV.Planned)} + {...tooltip(t("show.watchlistEdit"))} + {...props} + /> + ); + case WatchStatusV.Completed: + return ( + mutation.mutate(null)} + {...tooltip(t("show.watchlistRemove"))} + {...props} + /> + ); + case WatchStatusV.Droped: + return ( + mutation.mutate(WatchStatusV.Planned)} + {...tooltip(t("show.watchlistEdit"))} + {...props} + /> + ); + } +}; diff --git a/front/packages/ui/src/details/header.tsx b/front/packages/ui/src/details/header.tsx index 26eef47f..4a4b5dec 100644 --- a/front/packages/ui/src/details/header.tsx +++ b/front/packages/ui/src/details/header.tsx @@ -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 && ( + + )} {rating !== null && ( <> diff --git a/front/translations/en.json b/front/translations/en.json index 56ab9553..6722fe92 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -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}}", diff --git a/front/translations/fr.json b/front/translations/fr.json index 841fe87b..c63e2b7f 100644 --- a/front/translations/fr.json +++ b/front/translations/fr.json @@ -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}}",