feat(add watchlist): adding midding functionality from overserr

feat(add watchlist): adding missing functionality from overserr
This commit is contained in:
Yalagin
2023-04-27 21:19:36 +07:00
parent 53f6a890b9
commit 5f1c10d50a
30 changed files with 534 additions and 20 deletions

View File

@@ -338,6 +338,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
<TitleCard
key={`collection-movie-${title.id}`}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -57,7 +57,9 @@ const ListView = ({
case 'movie':
titleCard = (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -75,7 +77,9 @@ const ListView = ({
case 'tv':
titleCard = (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
discoverwatchlist: 'Your Plex Watchlist',
discoverwatchlist: 'Your Watchlist',
watchlist: 'Plex Watchlist',
});

View File

@@ -1,6 +1,6 @@
import Slider from '@app/components/Slider';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import { UserType, useUser } from '@app/hooks/useUser';
import { useUser } from '@app/hooks/useUser';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link';
@@ -8,7 +8,7 @@ import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
const messages = defineMessages({
plexwatchlist: 'Your Plex Watchlist',
plexwatchlist: 'Your Watchlist',
emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
});
@@ -22,12 +22,11 @@ const PlexWatchlistSlider = () => {
totalPages: number;
totalResults: number;
results: WatchlistItem[];
}>(user?.userType === UserType.PLEX ? '/api/v1/discover/watchlist' : null, {
}>('/api/v1/discover/watchlist', {
revalidateOnMount: true,
});
if (
user?.userType !== UserType.PLEX ||
(watchlistItems &&
watchlistItems.results.length === 0 &&
!user?.settings?.watchlistSyncMovies &&
@@ -69,6 +68,7 @@ const PlexWatchlistSlider = () => {
key={`watchlist-slider-item-${item.ratingKey}`}
tmdbId={item.tmdbId}
type={item.mediaType}
isAddedToWatchlist={true}
/>
))}
/>

View File

@@ -74,7 +74,7 @@ export const sliderTitles = defineMessages({
recentlyAdded: 'Recently Added',
upcoming: 'Upcoming Movies',
trending: 'Trending',
plexwatchlist: 'Your Plex Watchlist',
plexwatchlist: 'Your Watchlist',
moviegenres: 'Movie Genres',
tvgenres: 'Series Genres',
studios: 'Studios',

View File

@@ -95,7 +95,9 @@ const MediaSlider = ({
case 'movie':
return (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -109,7 +111,9 @@ const MediaSlider = ({
case 'tv':
return (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -62,7 +62,7 @@ const messages = defineMessages({
'Get notified when issues are reopened by other users.',
mediaautorequested: 'Request Automatically Submitted',
mediaautorequestedDescription:
'Get notified when new media requests are automatically submitted for items on your Plex Watchlist.',
'Get notified when new media requests are automatically submitted for items on Your Watchlist.',
});
export const hasNotificationType = (

View File

@@ -133,6 +133,7 @@ const PersonDetails = () => {
return (
<li key={`list-cast-item-${media.id}-${index}`}>
<TitleCard
key={media.id}
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}
@@ -173,6 +174,7 @@ const PersonDetails = () => {
return (
<li key={`list-crew-item-${media.id}-${index}`}>
<TitleCard
key={media.id}
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}

View File

@@ -11,6 +11,7 @@ export interface TmdbTitleCardProps {
tvdbId?: number;
type: 'movie' | 'tv';
canExpand?: boolean;
isAddedToWatchlist?: boolean;
}
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -23,6 +24,7 @@ const TmdbTitleCard = ({
tvdbId,
type,
canExpand,
isAddedToWatchlist = false,
}: TmdbTitleCardProps) => {
const { hasPermission } = useUser();
@@ -56,7 +58,11 @@ const TmdbTitleCard = ({
return isMovie(title) ? (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={
title.mediaInfo?.watchlists?.length || isAddedToWatchlist
}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -68,7 +74,11 @@ const TmdbTitleCard = ({
/>
) : (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={
title.mediaInfo?.watchlists?.length || isAddedToWatchlist
}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -10,12 +10,21 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { withProperties } from '@app/utils/typeHelpers';
import { Transition } from '@headlessui/react';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import {
ArrowDownTrayIcon,
MinusCircleIcon,
StarIcon,
} from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media';
import type { Watchlist } from '@server/entity/Watchlist';
import type { MediaType } from '@server/models/Search';
import axios from 'axios';
import Link from 'next/link';
import type React from 'react';
import { Fragment, useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import { mutate } from 'swr';
interface TitleCardProps {
id: number;
@@ -28,8 +37,19 @@ interface TitleCardProps {
status?: MediaStatus;
canExpand?: boolean;
inProgress?: boolean;
isAddedToWatchlist?: number | boolean;
}
const messages = defineMessages({
addToWatchList: 'Add to watchlist',
watchlistSuccess:
'<strong>{title}</strong> added to watchlist successfully!',
watchlistDeleted:
'<strong>{title}</strong> Removed from watchlist successfully!',
watchlistCancel: 'watchlist for <strong>{title}</strong> canceled.',
watchlistError: 'Something went wrong try again.',
});
const TitleCard = ({
id,
image,
@@ -38,6 +58,7 @@ const TitleCard = ({
title,
status,
mediaType,
isAddedToWatchlist = false,
inProgress = false,
canExpand = false,
}: TitleCardProps) => {
@@ -48,6 +69,10 @@ const TitleCard = ({
const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false);
const [showRequestModal, setShowRequestModal] = useState(false);
const { addToast } = useToasts();
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!isAddedToWatchlist
);
// Just to get the year from the date
if (year) {
@@ -68,6 +93,65 @@ const TitleCard = ({
[]
);
const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const response = await axios.post<Watchlist>('/api/v1/watchlist', {
tmdbId: id,
mediaType,
title,
});
mutate('/api/v1/discover/watchlist');
if (response.data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
} finally {
setIsUpdating(false);
setToggleWatchlist((prevState) => !prevState);
}
};
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const response = await axios.delete<Watchlist>('/api/v1/watchlist/' + id);
if (response.status === 204) {
addToast(
<span>
{intl.formatMessage(messages.watchlistDeleted, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
} finally {
setIsUpdating(false);
mutate('/api/v1/discover/watchlist');
setToggleWatchlist((prevState) => !prevState);
}
};
const closeModal = useCallback(() => setShowRequestModal(false), []);
const showRequestButton = hasPermission(
@@ -141,6 +225,28 @@ const TitleCard = ({
: intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
{showDetail && (
<>
{toggleWatchlist ? (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={onClickWatchlistBtn}
>
<StarIcon className={'h-3 text-amber-300'} />
</Button>
) : (
<Button
className="z-40"
buttonSize={'sm'}
onClick={onClickDeleteWatchlistBtn}
>
<MinusCircleIcon className={'h-3'} />
</Button>
)}
</>
)}
{currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
<div className="pointer-events-none z-40 flex items-center">
<StatusBadgeMini

View File

@@ -12,7 +12,7 @@
"components.Discover.DiscoverStudio.studioMovies": "{studio} Movies",
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Plex Watchlist",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
@@ -25,7 +25,7 @@
"components.Discover.discovertv": "Popular Series",
"components.Discover.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
"components.Discover.noRequests": "No requests.",
"components.Discover.plexwatchlist": "Your Plex Watchlist",
"components.Discover.plexwatchlist": "Your Watchlist",
"components.Discover.popularmovies": "Popular Movies",
"components.Discover.populartv": "Popular Series",
"components.Discover.recentlyAdded": "Recently Added",
@@ -210,7 +210,7 @@
"components.NotificationTypeSelector.mediaapproved": "Request Approved",
"components.NotificationTypeSelector.mediaapprovedDescription": "Send notifications when media requests are manually approved.",
"components.NotificationTypeSelector.mediaautorequested": "Request Automatically Submitted",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Get notified when new media requests are automatically submitted for items on your Plex Watchlist.",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Get notified when new media requests are automatically submitted for items on Your Watchlist.",
"components.NotificationTypeSelector.mediaavailable": "Request Available",
"components.NotificationTypeSelector.mediaavailableDescription": "Send notifications when media requests become available.",
"components.NotificationTypeSelector.mediadeclined": "Request Declined",

View File

@@ -12,7 +12,7 @@
"components.Discover.DiscoverStudio.studioMovies": "Фільми {studio}",
"components.Discover.DiscoverTvGenre.genreSeries": "Серіали в жанрі \"{genre}\"",
"components.Discover.DiscoverTvLanguage.languageSeries": "Серіали мовою \"{language}\"",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Plex Watchlist",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Список спостереження Plex",
"components.Discover.MovieGenreList.moviegenres": "Фільми за жанрами",
"components.Discover.MovieGenreSlider.moviegenres": "Фільми за жанрами",