import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; import ConfirmButton from '@app/components/Common/ConfirmButton'; import RequestModal from '@app/components/RequestModal'; import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import { ArrowPathIcon, CheckIcon, PencilIcon, TrashIcon, XMarkIcon, } from '@heroicons/react/24/solid'; import { MediaRequestStatus } from '@server/constants/media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import Link from 'next/link'; import { useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; const messages = defineMessages({ seasons: '{seasonCount, plural, one {Season} other {Seasons}}', failedretry: 'Something went wrong while retrying the request.', requested: 'Requested', requesteddate: 'Requested', modified: 'Modified', modifieduserdate: '{date} by {user}', mediaerror: '{mediaType} Not Found', editrequest: 'Edit Request', deleterequest: 'Delete Request', cancelRequest: 'Cancel Request', tmdbid: 'TMDB ID', tvdbid: 'TheTVDB ID', unknowntitle: 'Unknown Title', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; interface RequestItemErrorProps { requestData?: MediaRequest; revalidateList: () => void; } const RequestItemError = ({ requestData, revalidateList, }: RequestItemErrorProps) => { const intl = useIntl(); const { hasPermission } = useUser(); const deleteRequest = async () => { await axios.delete(`/api/v1/media/${requestData?.media.id}`); revalidateList(); }; const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ mediaUrl: requestData?.media?.mediaUrl, mediaUrl4k: requestData?.media?.mediaUrl4k, iOSPlexUrl: requestData?.media?.iOSPlexUrl, iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k, }); return (
{intl.formatMessage(messages.mediaerror, { mediaType: intl.formatMessage( requestData?.type ? requestData?.type === 'movie' ? globalMessages.movie : globalMessages.tvshow : globalMessages.request ), })}
{requestData && hasPermission(Permission.MANAGE_REQUESTS) && ( <>
{intl.formatMessage(messages.tmdbid)} {requestData.media.tmdbId}
{requestData.media.tvdbId && (
{intl.formatMessage(messages.tvdbid)} {requestData?.media.tvdbId}
)} )}
{requestData && ( <>
{intl.formatMessage(globalMessages.status)} {requestData.status === MediaRequestStatus.DECLINED || requestData.status === MediaRequestStatus.FAILED ? ( {requestData.status === MediaRequestStatus.DECLINED ? intl.formatMessage(globalMessages.declined) : intl.formatMessage(globalMessages.failed)} ) : ( 0 } is4k={requestData.is4k} mediaType={requestData.type} plexUrl={requestData.is4k ? plexUrl4k : plexUrl} serviceUrl={ requestData.is4k ? requestData.media.serviceUrl4k : requestData.media.serviceUrl } /> )}
{hasPermission( [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], { type: 'or' } ) ? ( <> {intl.formatMessage(messages.requested)} {intl.formatMessage(messages.modifieduserdate, { date: ( ), user: ( {requestData.requestedBy.displayName} ), })} ) : ( <> {intl.formatMessage(messages.requesteddate)} )}
{requestData.modifiedBy && (
{intl.formatMessage(messages.modified)} {intl.formatMessage(messages.modifieduserdate, { date: ( ), user: ( {requestData.modifiedBy.displayName} ), })}
)} )}
{hasPermission(Permission.MANAGE_REQUESTS) && requestData?.media.id && ( )}
); }; interface RequestItemProps { request: MediaRequest; revalidateList: () => void; } const RequestItem = ({ request, revalidateList }: RequestItemProps) => { const { ref, inView } = useInView({ triggerOnce: true, }); const { addToast } = useToasts(); const intl = useIntl(); const { user, hasPermission } = useUser(); const [showEditModal, setShowEditModal] = useState(false); const url = request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; const { data: title, error } = useSWR( inView ? url : null ); const { data: requestData, mutate: revalidate } = useSWR( `/api/v1/request/${request.id}`, { fallbackData: request, } ); const [isRetrying, setRetrying] = useState(false); const modifyRequest = async (type: 'approve' | 'decline') => { const response = await axios.post(`/api/v1/request/${request.id}/${type}`); if (response) { revalidate(); } }; const deleteRequest = async () => { await axios.delete(`/api/v1/request/${request.id}`); revalidateList(); }; const retryRequest = async () => { setRetrying(true); try { const result = await axios.post(`/api/v1/request/${request.id}/retry`); revalidate(result.data); } catch (e) { addToast(intl.formatMessage(messages.failedretry), { autoDismiss: true, appearance: 'error', }); } finally { setRetrying(false); } }; const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ mediaUrl: requestData?.media?.mediaUrl, mediaUrl4k: requestData?.media?.mediaUrl4k, iOSPlexUrl: requestData?.media?.iOSPlexUrl, iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k, }); if (!title && !error) { return (
); } if (!title || !requestData) { return ( ); } return ( <> setShowEditModal(false)} onComplete={() => { revalidateList(); setShowEditModal(false); }} />
{title.backdropPath && (
)}
{(isMovie(title) ? title.releaseDate : title.firstAirDate )?.slice(0, 4)}
{isMovie(title) ? title.title : title.name} {!isMovie(title) && request.seasons.length > 0 && (
{intl.formatMessage(messages.seasons, { seasonCount: title.seasons.filter( (season) => season.seasonNumber !== 0 ).length === request.seasons.length ? 0 : request.seasons.length, })}
{request.seasons.map((season) => ( {season.seasonNumber} ))}
)}
{intl.formatMessage(globalMessages.status)} {requestData.status === MediaRequestStatus.DECLINED ? ( {intl.formatMessage(globalMessages.declined)} ) : requestData.status === MediaRequestStatus.FAILED ? ( {intl.formatMessage(globalMessages.failed)} ) : ( 0 } is4k={requestData.is4k} tmdbId={requestData.media.tmdbId} mediaType={requestData.type} plexUrl={requestData.is4k ? plexUrl4k : plexUrl} serviceUrl={ requestData.is4k ? requestData.media.serviceUrl4k : requestData.media.serviceUrl } /> )}
{hasPermission( [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], { type: 'or' } ) ? ( <> {intl.formatMessage(messages.requested)} {intl.formatMessage(messages.modifieduserdate, { date: ( ), user: ( {requestData.requestedBy.displayName} ), })} ) : ( <> {intl.formatMessage(messages.requesteddate)} )}
{requestData.modifiedBy && (
{intl.formatMessage(messages.modified)} {intl.formatMessage(messages.modifieduserdate, { date: ( ), user: ( {requestData.modifiedBy.displayName} ), })}
)}
{requestData.status === MediaRequestStatus.FAILED && hasPermission(Permission.MANAGE_REQUESTS) && ( )} {requestData.status !== MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && ( deleteRequest()} confirmText={intl.formatMessage(globalMessages.areyousure)} className="w-full" > {intl.formatMessage(messages.deleterequest)} )} {requestData.status === MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && (
)} {requestData.status === MediaRequestStatus.PENDING && (hasPermission(Permission.MANAGE_REQUESTS) || (requestData.requestedBy.id === user?.id && (requestData.type === 'tv' || hasPermission(Permission.REQUEST_ADVANCED)))) && ( )} {requestData.status === MediaRequestStatus.PENDING && !hasPermission(Permission.MANAGE_REQUESTS) && requestData.requestedBy.id === user?.id && ( deleteRequest()} confirmText={intl.formatMessage(globalMessages.areyousure)} className="w-full" > {intl.formatMessage(messages.cancelRequest)} )}
); }; export default RequestItem;