mirror of
https://github.com/sct/overseerr.git
synced 2025-12-27 08:45:06 +01:00
* feat: add support for requesting "Specials" for TV Shows This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These shows have Specials that are critical to understanding the plot of a TV show. fix #779 * chore(yarn.lock): undo inappropriate changes to yarn.lock I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the changes that ended up being made to the yarn.lock file. This commit is responsible, then, for undoing the changes to the yarn.lock file that ended up being submitted. * refactor: change loose equality to strict equality I received a comment from OwsleyJr pointing out that we are using loose equality when we could alternatively just be using strict equality to increase the robustness of our code. This commit does exactly that by squashing out previous usages of loose equality in my commits and replacing them with strict equality * refactor: move 'Specials' string to a global message Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR. Instead, we can just move it as a global message. This commit does exactly that. It squashes out and previous declarations of the 'Specials' string inside the src files, and moves it directly to the global messages file.
736 lines
28 KiB
TypeScript
736 lines
28 KiB
TypeScript
import Alert from '@app/components/Common/Alert';
|
|
import Badge from '@app/components/Common/Badge';
|
|
import Modal from '@app/components/Common/Modal';
|
|
import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester';
|
|
import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester';
|
|
import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay';
|
|
import SearchByNameModal from '@app/components/RequestModal/SearchByNameModal';
|
|
import useSettings from '@app/hooks/useSettings';
|
|
import { useUser } from '@app/hooks/useUser';
|
|
import globalMessages from '@app/i18n/globalMessages';
|
|
import defineMessages from '@app/utils/defineMessages';
|
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
|
import type SeasonRequest from '@server/entity/SeasonRequest';
|
|
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
|
import { Permission } from '@server/lib/permissions';
|
|
import type { TvDetails } from '@server/models/Tv';
|
|
import axios from 'axios';
|
|
import { useState } from 'react';
|
|
import { useIntl } from 'react-intl';
|
|
import { useToasts } from 'react-toast-notifications';
|
|
import useSWR, { mutate } from 'swr';
|
|
|
|
const messages = defineMessages('components.RequestModal', {
|
|
requestadmin: 'This request will be approved automatically.',
|
|
requestSuccess: '<strong>{title}</strong> requested successfully!',
|
|
requestseriestitle: 'Request Series',
|
|
requestseries4ktitle: 'Request Series in 4K',
|
|
edit: 'Edit Request',
|
|
approve: 'Approve Request',
|
|
cancel: 'Cancel Request',
|
|
pendingrequest: 'Pending Request',
|
|
pending4krequest: 'Pending 4K Request',
|
|
requestfrom: "{username}'s request is pending approval.",
|
|
requestseasons:
|
|
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
|
|
requestseasons4k:
|
|
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K',
|
|
alreadyrequested: 'Already Requested',
|
|
selectseason: 'Select Season(s)',
|
|
season: 'Season',
|
|
numberofepisodes: '# of Episodes',
|
|
seasonnumber: 'Season {number}',
|
|
errorediting: 'Something went wrong while editing the request.',
|
|
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
|
|
requestApproved: 'Request for <strong>{title}</strong> approved!',
|
|
requestcancelled: 'Request for <strong>{title}</strong> canceled.',
|
|
autoapproval: 'Automatic Approval',
|
|
requesterror: 'Something went wrong while submitting the request.',
|
|
pendingapproval: 'Your request is pending approval.',
|
|
});
|
|
|
|
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
tmdbId: number;
|
|
onCancel?: () => void;
|
|
onComplete?: (newStatus: MediaStatus) => void;
|
|
onUpdating?: (isUpdating: boolean) => void;
|
|
is4k?: boolean;
|
|
editRequest?: NonFunctionProperties<MediaRequest>;
|
|
}
|
|
|
|
const TvRequestModal = ({
|
|
onCancel,
|
|
onComplete,
|
|
tmdbId,
|
|
onUpdating,
|
|
editRequest,
|
|
is4k = false,
|
|
}: RequestModalProps) => {
|
|
const settings = useSettings();
|
|
const { addToast } = useToasts();
|
|
const editingSeasons: number[] = (editRequest?.seasons ?? []).map(
|
|
(season) => season.seasonNumber
|
|
);
|
|
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
|
|
const [requestOverrides, setRequestOverrides] =
|
|
useState<RequestOverrides | null>(null);
|
|
const [selectedSeasons, setSelectedSeasons] = useState<number[]>(
|
|
editRequest ? editingSeasons : []
|
|
);
|
|
const intl = useIntl();
|
|
const { user, hasPermission } = useUser();
|
|
const [searchModal, setSearchModal] = useState<{
|
|
show: boolean;
|
|
}>({
|
|
show: true,
|
|
});
|
|
const [tvdbId, setTvdbId] = useState<number | undefined>(undefined);
|
|
const { data: quota } = useSWR<QuotaResponse>(
|
|
user &&
|
|
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
|
|
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
|
|
: null
|
|
);
|
|
|
|
const currentlyRemaining =
|
|
(quota?.tv.remaining ?? 0) -
|
|
selectedSeasons.length +
|
|
(editRequest?.seasons ?? []).length;
|
|
|
|
const updateRequest = async (alsoApproveRequest = false) => {
|
|
if (!editRequest) {
|
|
return;
|
|
}
|
|
|
|
if (onUpdating) {
|
|
onUpdating(true);
|
|
mutate('/api/v1/request/count');
|
|
}
|
|
|
|
try {
|
|
if (selectedSeasons.length > 0) {
|
|
await axios.put(`/api/v1/request/${editRequest.id}`, {
|
|
mediaType: 'tv',
|
|
serverId: requestOverrides?.server,
|
|
profileId: requestOverrides?.profile,
|
|
rootFolder: requestOverrides?.folder,
|
|
languageProfileId: requestOverrides?.language,
|
|
userId: requestOverrides?.user?.id,
|
|
tags: requestOverrides?.tags,
|
|
seasons: selectedSeasons.sort((a, b) => a - b),
|
|
});
|
|
|
|
if (alsoApproveRequest) {
|
|
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
|
|
}
|
|
} else {
|
|
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
|
}
|
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
|
mutate('/api/v1/request/count');
|
|
|
|
addToast(
|
|
<span>
|
|
{selectedSeasons.length > 0
|
|
? intl.formatMessage(
|
|
alsoApproveRequest
|
|
? messages.requestApproved
|
|
: messages.requestedited,
|
|
{
|
|
title: data?.name,
|
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
|
}
|
|
)
|
|
: intl.formatMessage(messages.requestcancelled, {
|
|
title: data?.name,
|
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
|
})}
|
|
</span>,
|
|
{
|
|
appearance: 'success',
|
|
autoDismiss: true,
|
|
}
|
|
);
|
|
if (onComplete) {
|
|
onComplete(MediaStatus.PENDING);
|
|
}
|
|
} catch (e) {
|
|
addToast(<span>{intl.formatMessage(messages.errorediting)}</span>, {
|
|
appearance: 'error',
|
|
autoDismiss: true,
|
|
});
|
|
} finally {
|
|
if (onUpdating) {
|
|
onUpdating(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const sendRequest = async () => {
|
|
if (
|
|
settings.currentSettings.partialRequestsEnabled &&
|
|
selectedSeasons.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (onUpdating) {
|
|
onUpdating(true);
|
|
mutate('/api/v1/request/count');
|
|
}
|
|
|
|
try {
|
|
let overrideParams = {};
|
|
if (requestOverrides) {
|
|
overrideParams = {
|
|
serverId: requestOverrides.server,
|
|
profileId: requestOverrides.profile,
|
|
rootFolder: requestOverrides.folder,
|
|
languageProfileId: requestOverrides.language,
|
|
userId: requestOverrides?.user?.id,
|
|
tags: requestOverrides.tags,
|
|
};
|
|
}
|
|
const response = await axios.post<MediaRequest>('/api/v1/request', {
|
|
mediaId: data?.id,
|
|
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
|
|
mediaType: 'tv',
|
|
is4k,
|
|
seasons: settings.currentSettings.partialRequestsEnabled
|
|
? selectedSeasons.sort((a, b) => a - b)
|
|
: getAllSeasons().filter(
|
|
(season) =>
|
|
!getAllRequestedSeasons().includes(season) && season !== 0
|
|
),
|
|
...overrideParams,
|
|
});
|
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
|
|
|
if (response.data) {
|
|
if (onComplete) {
|
|
onComplete(response.data.media.status);
|
|
}
|
|
addToast(
|
|
<span>
|
|
{intl.formatMessage(messages.requestSuccess, {
|
|
title: data?.name,
|
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
|
})}
|
|
</span>,
|
|
{ appearance: 'success', autoDismiss: true }
|
|
);
|
|
}
|
|
} catch (e) {
|
|
addToast(intl.formatMessage(messages.requesterror), {
|
|
appearance: 'error',
|
|
autoDismiss: true,
|
|
});
|
|
} finally {
|
|
if (onUpdating) {
|
|
onUpdating(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getAllSeasons = (): number[] => {
|
|
return (data?.seasons ?? [])
|
|
.filter((season) => season.episodeCount !== 0)
|
|
.map((season) => season.seasonNumber);
|
|
};
|
|
|
|
const getAllRequestedSeasons = (): number[] => {
|
|
const requestedSeasons = (data?.mediaInfo?.requests ?? [])
|
|
.filter(
|
|
(request) =>
|
|
request.is4k === is4k &&
|
|
request.status !== MediaRequestStatus.DECLINED &&
|
|
request.status !== MediaRequestStatus.COMPLETED
|
|
)
|
|
.reduce((requestedSeasons, request) => {
|
|
return [
|
|
...requestedSeasons,
|
|
...request.seasons
|
|
.filter((season) => !editingSeasons.includes(season.seasonNumber))
|
|
.map((sr) => sr.seasonNumber),
|
|
];
|
|
}, [] as number[]);
|
|
|
|
const availableSeasons = (data?.mediaInfo?.seasons ?? [])
|
|
.filter(
|
|
(season) =>
|
|
(season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
|
|
season[is4k ? 'status4k' : 'status'] ===
|
|
MediaStatus.PARTIALLY_AVAILABLE ||
|
|
season[is4k ? 'status4k' : 'status'] === MediaStatus.PROCESSING) &&
|
|
!requestedSeasons.includes(season.seasonNumber)
|
|
)
|
|
.map((season) => season.seasonNumber);
|
|
|
|
return [...requestedSeasons, ...availableSeasons];
|
|
};
|
|
|
|
const isSelectedSeason = (seasonNumber: number): boolean =>
|
|
selectedSeasons.includes(seasonNumber);
|
|
|
|
const toggleSeason = (seasonNumber: number): void => {
|
|
// If this season already has a pending request, don't allow it to be toggled
|
|
if (getAllRequestedSeasons().includes(seasonNumber)) {
|
|
return;
|
|
}
|
|
|
|
// If there are no more remaining requests available, block toggle
|
|
if (
|
|
quota?.tv.limit &&
|
|
currentlyRemaining <= 0 &&
|
|
!isSelectedSeason(seasonNumber)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (selectedSeasons.includes(seasonNumber)) {
|
|
setSelectedSeasons((seasons) =>
|
|
seasons.filter((sn) => sn !== seasonNumber)
|
|
);
|
|
} else {
|
|
setSelectedSeasons((seasons) => [...seasons, seasonNumber]);
|
|
}
|
|
};
|
|
|
|
const unrequestedSeasons = getAllSeasons().filter((season) =>
|
|
!settings.currentSettings.partialRequestsEnabled
|
|
? !getAllRequestedSeasons().includes(season) && season !== 0
|
|
: !getAllRequestedSeasons().includes(season)
|
|
);
|
|
|
|
const toggleAllSeasons = (): void => {
|
|
// If the user has a quota and not enough requests for all seasons, block toggleAllSeasons
|
|
if (
|
|
quota?.tv.limit &&
|
|
(quota?.tv.remaining ?? 0) < unrequestedSeasons.length
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const standardUnrequestedSeasons = unrequestedSeasons.filter(
|
|
(seasonNumber) => seasonNumber !== 0
|
|
);
|
|
|
|
if (
|
|
data &&
|
|
selectedSeasons.length >= 0 &&
|
|
selectedSeasons.length < standardUnrequestedSeasons.length
|
|
) {
|
|
setSelectedSeasons(standardUnrequestedSeasons);
|
|
} else {
|
|
setSelectedSeasons([]);
|
|
}
|
|
};
|
|
|
|
const isAllSeasons = (): boolean => {
|
|
if (!data) {
|
|
return false;
|
|
}
|
|
return (
|
|
selectedSeasons.filter((season) => season !== 0).length ===
|
|
getAllSeasons().filter(
|
|
(season) => !getAllRequestedSeasons().includes(season) && season !== 0
|
|
).length
|
|
);
|
|
};
|
|
|
|
const getSeasonRequest = (
|
|
seasonNumber: number
|
|
): SeasonRequest | undefined => {
|
|
let seasonRequest: SeasonRequest | undefined;
|
|
|
|
if (
|
|
data?.mediaInfo &&
|
|
(data.mediaInfo.requests || []).filter(
|
|
(request) =>
|
|
request.is4k === is4k &&
|
|
request.status !== MediaRequestStatus.DECLINED &&
|
|
request.status !== MediaRequestStatus.COMPLETED
|
|
).length > 0
|
|
) {
|
|
data.mediaInfo.requests
|
|
.filter((request) => request.is4k === is4k)
|
|
.forEach((request) => {
|
|
if (!seasonRequest) {
|
|
seasonRequest = request.seasons.find(
|
|
(season) =>
|
|
season.seasonNumber === seasonNumber &&
|
|
season.status !== MediaRequestStatus.COMPLETED
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
return seasonRequest;
|
|
};
|
|
|
|
const isOwner = editRequest && editRequest.requestedBy.id === user?.id;
|
|
|
|
return data && !error && !data.externalIds.tvdbId && searchModal.show ? (
|
|
<SearchByNameModal
|
|
tvdbId={tvdbId}
|
|
setTvdbId={setTvdbId}
|
|
closeModal={() => setSearchModal({ show: false })}
|
|
onCancel={onCancel}
|
|
modalTitle={intl.formatMessage(
|
|
is4k ? messages.requestseries4ktitle : messages.requestseriestitle
|
|
)}
|
|
modalSubTitle={data.name}
|
|
tmdbId={tmdbId}
|
|
backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
|
|
/>
|
|
) : (
|
|
<Modal
|
|
loading={!data && !error}
|
|
backgroundClickable
|
|
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
|
|
onOk={() =>
|
|
editRequest
|
|
? hasPermission(Permission.MANAGE_REQUESTS)
|
|
? updateRequest(true)
|
|
: updateRequest()
|
|
: sendRequest()
|
|
}
|
|
title={intl.formatMessage(
|
|
editRequest
|
|
? is4k
|
|
? messages.pending4krequest
|
|
: messages.pendingrequest
|
|
: is4k
|
|
? messages.requestseries4ktitle
|
|
: messages.requestseriestitle
|
|
)}
|
|
subTitle={data?.name}
|
|
okText={
|
|
editRequest
|
|
? selectedSeasons.length === 0
|
|
? intl.formatMessage(messages.cancel)
|
|
: hasPermission(Permission.MANAGE_REQUESTS)
|
|
? intl.formatMessage(messages.approve)
|
|
: intl.formatMessage(messages.edit)
|
|
: getAllRequestedSeasons().length >= getAllSeasons().length
|
|
? intl.formatMessage(messages.alreadyrequested)
|
|
: !settings.currentSettings.partialRequestsEnabled
|
|
? intl.formatMessage(
|
|
is4k ? globalMessages.request4k : globalMessages.request
|
|
)
|
|
: selectedSeasons.length === 0
|
|
? intl.formatMessage(messages.selectseason)
|
|
: intl.formatMessage(
|
|
is4k ? messages.requestseasons4k : messages.requestseasons,
|
|
{
|
|
seasonCount: selectedSeasons.length,
|
|
}
|
|
)
|
|
}
|
|
okDisabled={
|
|
editRequest
|
|
? false
|
|
: !settings.currentSettings.partialRequestsEnabled &&
|
|
quota?.tv.limit &&
|
|
unrequestedSeasons.length > quota.tv.limit
|
|
? true
|
|
: getAllRequestedSeasons().length >= getAllSeasons().length ||
|
|
(settings.currentSettings.partialRequestsEnabled &&
|
|
selectedSeasons.length === 0)
|
|
}
|
|
okButtonType={
|
|
editRequest
|
|
? settings.currentSettings.partialRequestsEnabled &&
|
|
selectedSeasons.length === 0
|
|
? 'danger'
|
|
: hasPermission(Permission.MANAGE_REQUESTS)
|
|
? 'success'
|
|
: 'primary'
|
|
: 'primary'
|
|
}
|
|
cancelText={
|
|
editRequest
|
|
? intl.formatMessage(globalMessages.close)
|
|
: tvdbId
|
|
? intl.formatMessage(globalMessages.back)
|
|
: intl.formatMessage(globalMessages.cancel)
|
|
}
|
|
backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
|
|
>
|
|
{editRequest
|
|
? isOwner
|
|
? intl.formatMessage(messages.pendingapproval)
|
|
: intl.formatMessage(messages.requestfrom, {
|
|
username: editRequest?.requestedBy.displayName,
|
|
})
|
|
: null}
|
|
{hasPermission(
|
|
[
|
|
Permission.MANAGE_REQUESTS,
|
|
is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE,
|
|
is4k ? Permission.AUTO_APPROVE_4K_TV : Permission.AUTO_APPROVE_TV,
|
|
],
|
|
{ type: 'or' }
|
|
) &&
|
|
!(
|
|
quota?.tv.limit &&
|
|
!settings.currentSettings.partialRequestsEnabled &&
|
|
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
|
) &&
|
|
getAllRequestedSeasons().length < getAllSeasons().length &&
|
|
!editRequest && (
|
|
<p className="mt-6">
|
|
<Alert
|
|
title={intl.formatMessage(messages.requestadmin)}
|
|
type="info"
|
|
/>
|
|
</p>
|
|
)}
|
|
{(quota?.tv.limit ?? 0) > 0 && (
|
|
<QuotaDisplay
|
|
mediaType="tv"
|
|
quota={quota?.tv}
|
|
remaining={
|
|
!settings.currentSettings.partialRequestsEnabled &&
|
|
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
|
? 0
|
|
: currentlyRemaining
|
|
}
|
|
userOverride={
|
|
requestOverrides?.user && requestOverrides.user.id !== user?.id
|
|
? requestOverrides?.user?.id
|
|
: undefined
|
|
}
|
|
overLimit={
|
|
!settings.currentSettings.partialRequestsEnabled &&
|
|
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
|
? unrequestedSeasons.length
|
|
: undefined
|
|
}
|
|
/>
|
|
)}
|
|
<div className="flex flex-col">
|
|
<div className="-mx-4 sm:mx-0">
|
|
<div className="inline-block min-w-full py-2 align-middle">
|
|
<div className="overflow-hidden border border-gray-700 shadow backdrop-blur sm:rounded-lg">
|
|
<table className="min-w-full">
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
className={`w-16 bg-gray-700 bg-opacity-80 px-4 py-3 ${
|
|
!settings.currentSettings.partialRequestsEnabled &&
|
|
'hidden'
|
|
}`}
|
|
>
|
|
<span
|
|
role="checkbox"
|
|
tabIndex={0}
|
|
aria-checked={isAllSeasons()}
|
|
onClick={() => toggleAllSeasons()}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === 'Space') {
|
|
toggleAllSeasons();
|
|
}
|
|
}}
|
|
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
|
quota?.tv.remaining &&
|
|
quota.tv.limit &&
|
|
quota.tv.remaining < unrequestedSeasons.length
|
|
? 'opacity-50'
|
|
: ''
|
|
}`}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`${
|
|
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
|
|
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
|
></span>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`${
|
|
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
|
|
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
|
></span>
|
|
</span>
|
|
</th>
|
|
<th className="bg-gray-700 bg-opacity-80 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
|
{intl.formatMessage(messages.season)}
|
|
</th>
|
|
<th className="bg-gray-700 bg-opacity-80 px-5 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
|
{intl.formatMessage(messages.numberofepisodes)}
|
|
</th>
|
|
<th className="bg-gray-700 bg-opacity-80 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
|
{intl.formatMessage(globalMessages.status)}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-700">
|
|
{data?.seasons
|
|
.filter((season) => season.episodeCount !== 0)
|
|
.map((season) => {
|
|
const seasonRequest = getSeasonRequest(
|
|
season.seasonNumber
|
|
);
|
|
const mediaSeason = data?.mediaInfo?.seasons.find(
|
|
(sn) =>
|
|
sn.seasonNumber === season.seasonNumber &&
|
|
sn[is4k ? 'status4k' : 'status'] !==
|
|
MediaStatus.UNKNOWN &&
|
|
sn[is4k ? 'status4k' : 'status'] !==
|
|
MediaStatus.DELETED
|
|
);
|
|
return (
|
|
<tr key={`season-${season.id}`}>
|
|
<td
|
|
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
|
|
!settings.currentSettings
|
|
.partialRequestsEnabled && 'hidden'
|
|
}`}
|
|
>
|
|
<span
|
|
role="checkbox"
|
|
tabIndex={0}
|
|
aria-checked={
|
|
!!mediaSeason ||
|
|
(!!seasonRequest &&
|
|
!editingSeasons.includes(
|
|
season.seasonNumber
|
|
)) ||
|
|
isSelectedSeason(season.seasonNumber)
|
|
}
|
|
onClick={() => toggleSeason(season.seasonNumber)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === 'Space') {
|
|
toggleSeason(season.seasonNumber);
|
|
}
|
|
}}
|
|
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
|
mediaSeason ||
|
|
(quota?.tv.limit &&
|
|
currentlyRemaining <= 0 &&
|
|
!isSelectedSeason(season.seasonNumber)) ||
|
|
(!!seasonRequest &&
|
|
!editingSeasons.includes(season.seasonNumber))
|
|
? 'opacity-50'
|
|
: ''
|
|
}`}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`${
|
|
!!mediaSeason ||
|
|
(!!seasonRequest &&
|
|
!editingSeasons.includes(
|
|
season.seasonNumber
|
|
)) ||
|
|
isSelectedSeason(season.seasonNumber)
|
|
? 'bg-indigo-500'
|
|
: 'bg-gray-700'
|
|
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
|
></span>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`${
|
|
!!mediaSeason ||
|
|
(!!seasonRequest &&
|
|
!editingSeasons.includes(
|
|
season.seasonNumber
|
|
)) ||
|
|
isSelectedSeason(season.seasonNumber)
|
|
? 'translate-x-5'
|
|
: 'translate-x-0'
|
|
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
|
></span>
|
|
</span>
|
|
</td>
|
|
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
|
{season.seasonNumber === 0
|
|
? intl.formatMessage(globalMessages.specials)
|
|
: intl.formatMessage(messages.seasonnumber, {
|
|
number: season.seasonNumber,
|
|
})}
|
|
</td>
|
|
<td className="whitespace-nowrap px-5 py-4 text-sm leading-5 text-gray-200 md:px-6">
|
|
{season.episodeCount}
|
|
</td>
|
|
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
|
|
{!seasonRequest && !mediaSeason && (
|
|
<Badge>
|
|
{intl.formatMessage(
|
|
globalMessages.notrequested
|
|
)}
|
|
</Badge>
|
|
)}
|
|
{!mediaSeason &&
|
|
seasonRequest?.status ===
|
|
MediaRequestStatus.PENDING && (
|
|
<Badge badgeType="warning">
|
|
{intl.formatMessage(globalMessages.pending)}
|
|
</Badge>
|
|
)}
|
|
{((!mediaSeason &&
|
|
seasonRequest?.status ===
|
|
MediaRequestStatus.APPROVED) ||
|
|
mediaSeason?.[is4k ? 'status4k' : 'status'] ===
|
|
MediaStatus.PROCESSING) && (
|
|
<Badge badgeType="primary">
|
|
{intl.formatMessage(globalMessages.requested)}
|
|
</Badge>
|
|
)}
|
|
{mediaSeason?.[is4k ? 'status4k' : 'status'] ===
|
|
MediaStatus.PARTIALLY_AVAILABLE && (
|
|
<Badge badgeType="success">
|
|
{intl.formatMessage(
|
|
globalMessages.partiallyavailable
|
|
)}
|
|
</Badge>
|
|
)}
|
|
{mediaSeason?.[is4k ? 'status4k' : 'status'] ===
|
|
MediaStatus.AVAILABLE && (
|
|
<Badge badgeType="success">
|
|
{intl.formatMessage(globalMessages.available)}
|
|
</Badge>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
|
<AdvancedRequester
|
|
type="tv"
|
|
is4k={is4k}
|
|
isAnime={data?.keywords.some(
|
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
|
)}
|
|
onChange={(overrides) => setRequestOverrides(overrides)}
|
|
requestUser={editRequest?.requestedBy}
|
|
defaultOverrides={
|
|
editRequest
|
|
? {
|
|
folder: editRequest.rootFolder,
|
|
profile: editRequest.profileId,
|
|
server: editRequest.serverId,
|
|
language: editRequest.languageProfileId,
|
|
tags: editRequest.tags,
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default TvRequestModal;
|