You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/src/components/ManageSlideOver/index.tsx

507 lines
20 KiB

import { ServerIcon, ViewListIcon } from '@heroicons/react/outline';
import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { IssueStatus } from '../../../server/constants/issue';
import {
MediaRequestStatus,
MediaStatus,
} from '../../../server/constants/media';
import type { MediaWatchDataResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import ConfirmButton from '../Common/ConfirmButton';
import SlideOver from '../Common/SlideOver';
import DownloadBlock from '../DownloadBlock';
import IssueBlock from '../IssueBlock';
import RequestBlock from '../RequestBlock';
const messages = defineMessages({
manageModalTitle: 'Manage {mediaType}',
manageModalIssues: 'Open Issues',
manageModalRequests: 'Requests',
manageModalMedia: 'Media',
manageModalMedia4k: '4K Media',
manageModalAdvanced: 'Advanced',
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
openarr: 'Open in {arr}',
openarr4k: 'Open in 4K {arr}',
downloadstatus: 'Downloads',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark as Available in 4K',
markallseasonsavailable: 'Mark All Seasons as Available',
markallseasons4kavailable: 'Mark All Seasons as Available in 4K',
opentautulli: 'Open in Tautulli',
plays:
'<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}',
pastdays: 'Past {days, number} Days',
alltime: 'All Time',
playedby: 'Played By',
movie: 'movie',
tvshow: 'series',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
interface ManageSlideOverProps {
// mediaType: 'movie' | 'tv';
show?: boolean;
onClose: () => void;
revalidate: () => void;
}
interface ManageSlideOverMovieProps extends ManageSlideOverProps {
mediaType: 'movie';
data: MovieDetails;
}
interface ManageSlideOverTvProps extends ManageSlideOverProps {
mediaType: 'tv';
data: TvDetails;
}
const ManageSlideOver: React.FC<
ManageSlideOverMovieProps | ManageSlideOverTvProps
> = ({ show, mediaType, onClose, data, revalidate }) => {
const { user: currentUser, hasPermission } = useUser();
const intl = useIntl();
const settings = useSettings();
const { data: watchData } = useSWR<MediaWatchDataResponse>(
data.mediaInfo && hasPermission(Permission.ADMIN)
? `/api/v1/media/${data.mediaInfo.id}/watch_data`
: null
);
const deleteMedia = async () => {
if (data.mediaInfo) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
}
};
const markAvailable = async (is4k = false) => {
if (data.mediaInfo) {
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
is4k,
});
revalidate();
}
};
const requests =
data.mediaInfo?.requests?.filter(
(request) => request.status !== MediaRequestStatus.DECLINED
) ?? [];
const openIssues =
data.mediaInfo?.issues?.filter(
(issue) => issue.status === IssueStatus.OPEN
) ?? [];
const styledPlayCount = (playCount: number): JSX.Element => {
return (
<>
{intl.formatMessage(messages.plays, {
playCount,
strong: (msg: React.ReactNode) => (
<strong className="text-2xl font-semibold">{msg}</strong>
),
})}
</>
);
};
return (
<SlideOver
show={show}
title={intl.formatMessage(messages.manageModalTitle, {
mediaType: intl.formatMessage(
mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow
),
})}
onClose={() => onClose()}
subText={isMovie(data) ? data.title : data.name}
>
<div className="space-y-6">
{((data?.mediaInfo?.downloadStatus ?? []).length > 0 ||
(data?.mediaInfo?.downloadStatus4k ?? []).length > 0) && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.downloadstatus)}
</h3>
<div className="overflow-hidden rounded-md bg-gray-600 shadow">
<ul>
{data.mediaInfo?.downloadStatus?.map((status, index) => (
<li
key={`dl-status-${status.externalId}-${index}`}
className="border-b border-gray-700 last:border-b-0"
>
<DownloadBlock downloadItem={status} />
</li>
))}
{data.mediaInfo?.downloadStatus4k?.map((status, index) => (
<li
key={`dl-status-${status.externalId}-${index}`}
className="border-b border-gray-700 last:border-b-0"
>
<DownloadBlock downloadItem={status} is4k />
</li>
))}
</ul>
</div>
</div>
)}
{hasPermission([Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES], {
type: 'or',
}) &&
openIssues.length > 0 && (
<>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalIssues)}
</h3>
<div className="overflow-hidden rounded-md bg-gray-600 shadow">
<ul>
{openIssues.map((issue) => (
<li
key={`manage-issue-${issue.id}`}
className="border-b border-gray-700 last:border-b-0"
>
<IssueBlock issue={issue} />
</li>
))}
</ul>
</div>
</>
)}
{requests.length > 0 && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalRequests)}
</h3>
<div className="overflow-hidden rounded-md bg-gray-600 shadow">
<ul>
{requests.map((request) => (
<li
key={`manage-request-${request.id}`}
className="border-b border-gray-700 last:border-b-0"
>
<RequestBlock
request={request}
onUpdate={() => revalidate()}
/>
</li>
))}
</ul>
</div>
</div>
)}
{hasPermission(Permission.ADMIN) &&
(data.mediaInfo?.serviceUrl ||
data.mediaInfo?.tautulliUrl ||
!!watchData?.data?.playCount) && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalMedia)}
</h3>
<div className="space-y-2">
{!!watchData?.data && (
<div>
<div
className={`grid grid-cols-1 divide-y divide-gray-500 overflow-hidden bg-gray-600 text-sm text-gray-300 shadow ${
data.mediaInfo?.tautulliUrl
? 'rounded-t-md'
: 'rounded-md'
}`}
>
<div className="grid grid-cols-3 divide-x divide-gray-500">
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.pastdays, { days: 7 })}
</div>
<div className="text-white">
{styledPlayCount(watchData.data.playCount7Days)}
</div>
</div>
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.pastdays, {
days: 30,
})}
</div>
<div className="text-white">
{styledPlayCount(watchData.data.playCount30Days)}
</div>
</div>
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.alltime)}
</div>
<div className="text-white">
{styledPlayCount(watchData.data.playCount)}
</div>
</div>
</div>
{!!watchData.data.users.length && (
<div className="flex flex-row space-x-2 px-4 pt-3 pb-2">
<span className="shrink-0 font-bold leading-8">
{intl.formatMessage(messages.playedby)}
</span>
<span className="flex flex-row flex-wrap">
{watchData.data.users.map((user) => (
<Link
href={
currentUser?.id === user.id
? '/profile'
: `/users/${user.id}`
}
key={`watch-user-${user.id}`}
>
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50">
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>
))}
</span>
</div>
)}
</div>
{data.mediaInfo?.tautulliUrl && (
<a
href={data.mediaInfo.tautulliUrl}
target="_blank"
rel="noreferrer"
>
<Button
buttonType="ghost"
className={`w-full ${
watchData.data.playCount ? 'rounded-t-none' : ''
}`}
>
<ViewListIcon />
<span>
{intl.formatMessage(messages.opentautulli)}
</span>
</Button>
</a>
)}
</div>
)}
{data?.mediaInfo?.serviceUrl && (
<a
href={data?.mediaInfo?.serviceUrl}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
)}
</div>
</div>
)}
{hasPermission(Permission.ADMIN) &&
(data.mediaInfo?.serviceUrl4k ||
data.mediaInfo?.tautulliUrl4k ||
!!watchData?.data4k?.playCount) && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalMedia4k)}
</h3>
<div className="space-y-2">
{!!watchData?.data4k && (
<div>
<div
className={`grid grid-cols-1 divide-y divide-gray-500 overflow-hidden bg-gray-600 text-sm text-gray-300 shadow ${
data.mediaInfo?.tautulliUrl4k
? 'rounded-t-md'
: 'rounded-md'
}`}
>
<div className="grid grid-cols-3 divide-x divide-gray-500">
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.pastdays, { days: 7 })}
</div>
<div className="text-white">
{styledPlayCount(watchData.data4k.playCount7Days)}
</div>
</div>
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.pastdays, {
days: 30,
})}
</div>
<div className="text-white">
{styledPlayCount(watchData.data4k.playCount30Days)}
</div>
</div>
<div className="px-4 py-3">
<div className="font-bold">
{intl.formatMessage(messages.alltime)}
</div>
<div className="text-white">
{styledPlayCount(watchData.data4k.playCount)}
</div>
</div>
</div>
{!!watchData.data4k.users.length && (
<div className="flex flex-row space-x-2 px-4 pt-3 pb-2">
<span className="shrink-0 font-bold leading-8">
{intl.formatMessage(messages.playedby)}
</span>
<span className="flex flex-row flex-wrap">
{watchData.data4k.users.map((user) => (
<Link
href={
currentUser?.id === user.id
? '/profile'
: `/users/${user.id}`
}
key={`watch-user-${user.id}`}
>
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50">
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>
))}
</span>
</div>
)}
</div>
{data.mediaInfo?.tautulliUrl4k && (
<a
href={data.mediaInfo.tautulliUrl4k}
target="_blank"
rel="noreferrer"
>
<Button
buttonType="ghost"
className={`w-full ${
watchData.data4k.playCount ? 'rounded-t-none' : ''
}`}
>
<ViewListIcon />
<span>
{intl.formatMessage(messages.opentautulli)}
</span>
</Button>
</a>
)}
</div>
)}
{data?.mediaInfo?.serviceUrl4k && (
<a
href={data?.mediaInfo?.serviceUrl4k}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
)}
</div>
</div>
)}
{hasPermission(Permission.ADMIN) && data?.mediaInfo && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalAdvanced)}
</h3>
<div className="space-y-2">
{data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
<Button
onClick={() => markAvailable()}
className="w-full"
buttonType="success"
>
<CheckCircleIcon />
<span>
{intl.formatMessage(
mediaType === 'movie'
? messages.markavailable
: messages.markallseasonsavailable
)}
</span>
</Button>
)}
{data?.mediaInfo.status4k !== MediaStatus.AVAILABLE &&
settings.currentSettings.series4kEnabled && (
<Button
onClick={() => markAvailable(true)}
className="w-full"
buttonType="success"
>
<CheckCircleIcon />
<span>
{intl.formatMessage(
mediaType === 'movie'
? messages.mark4kavailable
: messages.markallseasons4kavailable
)}
</span>
</Button>
)}
<div>
<ConfirmButton
onClick={() => deleteMedia()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<DocumentRemoveIcon />
<span>
{intl.formatMessage(messages.manageModalClearMedia)}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(messages.manageModalClearMediaWarning, {
mediaType: intl.formatMessage(
mediaType === 'movie' ? messages.movie : messages.tvshow
),
})}
</div>
</div>
</div>
</div>
)}
</div>
</SlideOver>
);
};
export default ManageSlideOver;