mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add basic watch status button on details page
This commit is contained in:
parent
2f309440cc
commit
cfa12b0fed
@ -29,4 +29,5 @@ export * from "./studio";
|
||||
export * from "./episode";
|
||||
export * from "./season";
|
||||
export * from "./watch-info";
|
||||
export * from "./watch-status";
|
||||
export * from "./user";
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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>;
|
||||
|
102
front/packages/ui/src/components/watchlist-info.tsx
Normal file
102
front/packages/ui/src/components/watchlist-info.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
@ -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>
|
||||
|
@ -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}}",
|
||||
|
@ -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}}",
|
||||
|
Loading…
x
Reference in New Issue
Block a user