fix: fixed request card not displaying the requested season and episodes

When requested, the request card shows as {seasonCount, plural, one {Season}} and does not display
which season or episode was requested because it was still using the alpha request cards. This fixed
that issue
This commit is contained in:
Fallenbagel
2022-04-13 17:24:47 +05:00
parent 73672e29f8
commit b03b9b1dbb
7 changed files with 1074 additions and 685 deletions

View File

@@ -1,24 +1,38 @@
import React, { useContext, useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { TvDetails } from '../../../server/models/Tv';
import type { MovieDetails } from '../../../server/models/Movie';
import useSWR from 'swr';
import { LanguageContext } from '../../context/LanguageContext';
import { MediaRequestStatus } from '../../../server/constants/media';
import Badge from '../Common/Badge';
import { useUser, Permission } from '../../hooks/useUser';
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Button from '../Common/Button';
import { withProperties } from '../../utils/typeHelpers';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import {
MediaRequestStatus,
MediaStatus,
} 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 { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import { withProperties } from '../../utils/typeHelpers';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage';
import RequestModal from '../RequestModal';
import StatusBadge from '../StatusBadge';
const messages = defineMessages({
seasons: 'Seasons',
all: 'All',
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
failedretry: 'Something went wrong while retrying the request.',
mediaerror: 'The associated title for this request is no longer available.',
deleterequest: 'Delete Request',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -27,7 +41,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
const RequestCardPlaceholder: React.FC = () => {
return (
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
<div className="relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96">
<div className="w-20 sm:w-28">
<div className="w-full" style={{ paddingBottom: '150%' }} />
</div>
@@ -35,6 +49,45 @@ const RequestCardPlaceholder: React.FC = () => {
);
};
interface RequestCardErrorProps {
mediaId?: number;
}
const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
const { hasPermission } = useUser();
const intl = useIntl();
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${mediaId}`);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
return (
<div className="relative w-72 rounded-xl bg-gray-800 p-4 ring-1 ring-red-500 sm:w-96">
<div className="w-20 sm:w-28">
<div className="w-full" style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center px-10">
<div className="w-full whitespace-normal text-center text-xs text-gray-300 sm:text-sm">
{intl.formatMessage(messages.mediaerror)}
</div>
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
<Button
buttonType="danger"
buttonSize="sm"
className="mt-4"
onClick={() => deleteRequest()}
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</Button>
)}
</div>
</div>
</div>
</div>
);
};
interface RequestCardProps {
request: MediaRequest;
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
@@ -45,19 +98,21 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
triggerOnce: true,
});
const intl = useIntl();
const { hasPermission } = useUser();
const { locale } = useContext(LanguageContext);
const { user, hasPermission } = useUser();
const { addToast } = useToasts();
const [isRetrying, setRetrying] = useState(false);
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<MovieDetails | TvDetails>(
inView ? `${url}?language=${locale}` : null
inView ? `${url}` : null
);
const {
data: requestData,
error: requestError,
revalidate,
mutate: revalidate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
@@ -70,6 +125,30 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
}
};
const deleteRequest = async () => {
await axios.delete(`/api/v1/request/${request.id}`);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
const retryRequest = async () => {
setRetrying(true);
try {
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
if (response) {
revalidate();
}
} catch (e) {
addToast(intl.formatMessage(messages.failedretry), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setRetrying(false);
}
};
useEffect(() => {
if (title && onTitleData) {
onTitleData(request.id, title);
@@ -85,157 +164,251 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
}
if (!requestData && !requestError) {
return <RequestCardPlaceholder />;
return <RequestCardError />;
}
if (!title || !requestData) {
return <RequestCardPlaceholder />;
return <RequestCardError mediaId={requestData?.media.id} />;
}
return (
<div
className="relative flex p-4 text-gray-400 bg-gray-800 bg-center bg-cover rounded-md w-72 sm:w-96"
style={{
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
}}
>
<div className="flex flex-col flex-1 min-w-0 pr-4">
<h2 className="overflow-hidden text-base text-white cursor-pointer sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
<Link
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
as={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
{isMovie(title) ? title.title : title.name}
</Link>
</h2>
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="flex items-center group">
<img
src={requestData.requestedBy.avatar}
<>
<RequestModal
show={showEditModal}
tmdbId={request.media.tmdbId}
type={request.type}
is4k={request.is4k}
editRequest={request}
onCancel={() => setShowEditModal(false)}
onComplete={() => {
revalidate();
setShowEditModal(false);
}}
/>
<div className="relative flex w-72 overflow-hidden rounded-xl bg-gray-800 bg-cover bg-center p-4 text-gray-400 shadow ring-1 ring-gray-700 sm:w-96">
{title.backdropPath && (
<div className="absolute inset-0 z-0">
<CachedImage
alt=""
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
layout="fill"
objectFit="cover"
/>
<span className="text-xs truncate sm:text-sm group-hover:underline">
{requestData.requestedBy.displayName}
</span>
</a>
</Link>
{requestData.media.status && (
<div className="mt-1 sm:mt-2">
<StatusBadge
status={
requestData.is4k
? requestData.media.status4k
: requestData.media.status
}
is4k={requestData.is4k}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
}}
/>
</div>
)}
{request.seasons.length > 0 && (
<div className="items-center hidden mt-2 text-sm sm:flex">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{!isMovie(title) &&
title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(messages.all)}</Badge>
</span>
) : (
<div className="overflow-x-scroll hide-scrollbar">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
<div className="relative z-10 flex min-w-0 flex-1 flex-col pr-4">
<div className="hidden text-xs font-medium text-white sm:flex">
{(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice(
0,
4
)}
</div>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="flex items-end flex-1">
<span className="mr-2">
<Link
href={
request.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
{hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
) && (
<div className="card-field">
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="group flex items-center">
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm"
/>
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}
</span>
</a>
</Link>
</div>
)}
{!isMovie(title) && request.seasons.length > 0 && (
<div className="my-0.5 hidden items-center text-sm sm:my-1 sm:flex">
<span className="mr-2 font-bold ">
{intl.formatMessage(messages.seasons, {
seasonCount:
title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length
? 0
: request.seasons.length,
})}
</span>
{title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
</span>
) : (
<div className="hide-scrollbar overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
)}
<div className="mt-2 flex items-center text-sm sm:mt-1">
<span className="mr-2 hidden font-bold sm:block">
{intl.formatMessage(globalMessages.status)}
</span>
{requestData.status === MediaRequestStatus.DECLINED ? (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.declined)}
</Badge>
) : requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN ? (
<Badge
badgeType="danger"
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
) : (
<StatusBadge
status={
requestData.media[requestData.is4k ? 'status4k' : 'status']
}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={
requestData.media[
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
]
}
/>
)}
</div>
<div className="flex flex-1 items-end space-x-2">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="success"
buttonType="primary"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
disabled={isRetrying}
onClick={() => retryRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
<RefreshIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ marginRight: '0', animationDirection: 'reverse' }}
/>
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
</span>
</Button>
</span>
<span>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<Button
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<CheckIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => modifyRequest('decline')}
>
<XIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)) && (
<Button
buttonType="primary"
buttonSize="sm"
onClick={() => setShowEditModal(true)}
className={`${
hasPermission(Permission.MANAGE_REQUESTS) ? 'sm:hidden' : ''
}`}
>
<PencilIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.edit)}
</span>
</Button>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => modifyRequest('decline')}
onClick={() => deleteRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
<XIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.cancel)}
</span>
</Button>
</span>
</div>
)}
</div>
<div className="flex-shrink-0 w-20 sm:w-28">
)}
</div>
</div>
<Link
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
as={
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<img
src={
title.posterPath
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
/>
<a className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28">
<CachedImage
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
layout="responsive"
width={600}
height={900}
/>
</a>
</Link>
</div>
</div>
</>
);
};

View File

@@ -1,13 +1,15 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useContext, useState } from 'react';
import React, { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import {
defineMessages,
FormattedDate,
FormattedRelativeTime,
useIntl,
} from 'react-intl';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import {
@@ -17,25 +19,70 @@ import {
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
import type { MovieDetails } from '../../../../server/models/Movie';
import type { TvDetails } from '../../../../server/models/Tv';
import { LanguageContext } from '../../../context/LanguageContext';
import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import Table from '../../Common/Table';
import CachedImage from '../../Common/CachedImage';
import ConfirmButton from '../../Common/ConfirmButton';
import RequestModal from '../../RequestModal';
import StatusBadge from '../../StatusBadge';
const messages = defineMessages({
seasons: 'Seasons',
notavailable: 'N/A',
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: 'The associated title for this request is no longer available.',
editrequest: 'Edit Request',
deleterequest: 'Delete Request',
cancelRequest: 'Cancel Request',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
interface RequestItemErroProps {
mediaId?: number;
revalidateList: () => void;
}
const RequestItemError: React.FC<RequestItemErroProps> = ({
mediaId,
revalidateList,
}) => {
const intl = useIntl();
const { hasPermission } = useUser();
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${mediaId}`);
revalidateList();
};
return (
<div className="flex h-64 w-full flex-col items-center justify-center rounded-xl bg-gray-800 px-10 ring-1 ring-red-500 lg:flex-row xl:h-28">
<span className="text-center text-sm text-gray-300 lg:text-left">
{intl.formatMessage(messages.mediaerror)}
</span>
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
<div className="mt-4 lg:ml-4 lg:mt-0">
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</Button>
</div>
)}
</div>
);
};
interface RequestItemProps {
request: MediaRequest;
revalidateList: () => void;
@@ -50,23 +97,21 @@ const RequestItem: React.FC<RequestItemProps> = ({
});
const { addToast } = useToasts();
const intl = useIntl();
const { hasPermission } = useUser();
const { user, hasPermission } = useUser();
const [showEditModal, setShowEditModal] = useState(false);
const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? `${url}?language=${locale}` : null
inView ? url : null
);
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
`/api/v1/request/${request.id}`,
{
initialData: request,
}
);
const {
data: requestData,
revalidate,
mutate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
const [isRetrying, setRetrying] = useState(false);
@@ -89,7 +134,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
try {
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
mutate(result.data);
revalidate(result.data);
} catch (e) {
addToast(intl.formatMessage(messages.failedretry), {
autoDismiss: true,
@@ -102,22 +147,24 @@ const RequestItem: React.FC<RequestItemProps> = ({
if (!title && !error) {
return (
<tr className="w-full h-24 animate-pulse" ref={ref}>
<td colSpan={6}></td>
</tr>
<div
className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
ref={ref}
/>
);
}
if (!title || !requestData) {
return (
<tr className="w-full h-24 animate-pulse">
<td colSpan={6}></td>
</tr>
<RequestItemError
mediaId={requestData?.media.id}
revalidateList={revalidateList}
/>
);
}
return (
<tr className="relative w-full h-24 p-2">
<>
<RequestModal
show={showEditModal}
tmdbId={request.media.tmdbId}
@@ -130,28 +177,26 @@ const RequestItem: React.FC<RequestItemProps> = ({
setShowEditModal(false);
}}
/>
<Table.TD>
<div className="flex items-center">
<Link
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a className="flex-shrink-0 hidden mr-4 sm:block">
<img
src={
title.posterPath
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
/>
</a>
</Link>
<div className="flex-shrink overflow-hidden">
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
layout="fill"
objectFit="cover"
/>
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
}}
/>
</div>
)}
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
<Link
href={
requestData.type === 'movie'
@@ -159,219 +204,295 @@ const RequestItem: React.FC<RequestItemProps> = ({
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="flex items-center mt-1">
<img
src={requestData.requestedBy.avatar}
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
<CachedImage
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
className="w-5 mr-2 rounded-full"
layout="responsive"
width={600}
height={900}
objectFit="cover"
/>
<span className="text-sm hover:underline">
{requestData.requestedBy.displayName}
</span>
</a>
</Link>
{requestData.seasons.length > 0 && (
<div className="items-center hidden mt-2 text-sm sm:flex">
<span className="mr-2">
{intl.formatMessage(messages.seasons)}
</span>
{requestData.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
{(isMovie(title)
? title.releaseDate
: title.firstAirDate
)?.slice(0, 4)}
</div>
<Link
href={
requestData.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
{!isMovie(title) && request.seasons.length > 0 && (
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.seasons, {
seasonCount:
title.seasons.filter(
(season) => season.seasonNumber !== 0
).length === request.seasons.length
? 0
: request.seasons.length,
})}
</span>
))}
{title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
</span>
) : (
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
)}
</div>
</div>
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(globalMessages.status)}
</span>
{requestData.status === MediaRequestStatus.DECLINED ? (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.declined)}
</Badge>
) : requestData.media[
requestData.is4k ? 'status4k' : 'status'
] === MediaStatus.UNKNOWN ? (
<Badge
badgeType="danger"
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
) : (
<StatusBadge
status={
requestData.media[requestData.is4k ? 'status4k' : 'status']
}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={
requestData.media[
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
]
}
/>
)}
</div>
<div className="card-field">
{hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
) ? (
<>
<span className="card-field-name">
{intl.formatMessage(messages.requested)}
</span>
<span className="flex truncate text-sm text-gray-300">
{intl.formatMessage(messages.modifieduserdate, {
date: (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.createdAt).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
),
user: (
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="group flex items-center truncate">
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}
</span>
</a>
</Link>
),
})}
</span>
</>
) : (
<>
<span className="card-field-name">
{intl.formatMessage(messages.requesteddate)}
</span>
<span className="flex truncate text-sm text-gray-300">
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.createdAt).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
</span>
</>
)}
</div>
{requestData.modifiedBy && (
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.modified)}
</span>
<span className="flex truncate text-sm text-gray-300">
{intl.formatMessage(messages.modifieduserdate, {
date: (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
),
user: (
<Link href={`/users/${requestData.modifiedBy.id}`}>
<a className="group flex items-center truncate">
<img
src={requestData.modifiedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.modifiedBy.displayName}
</span>
</a>
</Link>
),
})}
</span>
</div>
)}
</div>
</div>
</Table.TD>
<Table.TD>
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN ||
requestData.status === MediaRequestStatus.DECLINED ? (
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
) : (
<StatusBadge
status={requestData.media[requestData.is4k ? 'status4k' : 'status']}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
is4k={requestData.is4k}
/>
)}
</Table.TD>
<Table.TD>
<div className="flex flex-col">
<span className="text-sm text-gray-300">
<FormattedDate value={requestData.createdAt} />
</span>
</div>
</Table.TD>
<Table.TD>
<div className="flex flex-col">
{requestData.modifiedBy ? (
<span className="text-sm text-gray-300">
<div className="flex items-center">
<img
src={requestData.modifiedBy.avatar}
alt=""
className="w-5 mr-2 rounded-full"
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
className="w-full"
buttonType="primary"
disabled={isRetrying}
onClick={() => retryRequest()}
>
<RefreshIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ animationDirection: 'reverse' }}
/>
<span className="text-sm">
{requestData.modifiedBy.displayName} (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
)
<span>
{intl.formatMessage(
isRetrying ? globalMessages.retrying : globalMessages.retry
)}
</span>
</Button>
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</ConfirmButton>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="flex w-full flex-row space-x-2">
<span className="w-full">
<Button
className="w-full"
buttonType="success"
onClick={() => modifyRequest('approve')}
>
<CheckIcon />
<span>{intl.formatMessage(globalMessages.approve)}</span>
</Button>
</span>
<span className="w-full">
<Button
className="w-full"
buttonType="danger"
onClick={() => modifyRequest('decline')}
>
<XIcon />
<span>{intl.formatMessage(globalMessages.decline)}</span>
</Button>
</span>
</div>
</span>
) : (
<span className="text-sm text-gray-300">N/A</span>
)}
</div>
</Table.TD>
<Table.TD alignText="right">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
className="mr-2"
buttonType="primary"
buttonSize="sm"
disabled={isRetrying}
onClick={() => retryRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18px"
height="18px"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
</span>
</Button>
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.delete)}
</span>
</Button>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<span className="mr-2">
<Button
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
<span className="mr-2">
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => modifyRequest('decline')}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
<span>
)}
{requestData.status === MediaRequestStatus.PENDING &&
(hasPermission(Permission.MANAGE_REQUESTS) ||
(requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
<span className="w-full">
<Button
className="w-full"
buttonType="primary"
buttonSize="sm"
onClick={() => setShowEditModal(true)}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.edit)}
</span>
<PencilIcon />
<span>{intl.formatMessage(messages.editrequest)}</span>
</Button>
</span>
</>
)}
</Table.TD>
</tr>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<XIcon />
<span>{intl.formatMessage(messages.cancelRequest)}</span>
</ConfirmButton>
)}
</div>
</div>
</>
);
};

View File

@@ -1,51 +1,98 @@
import React, { useState } from 'react';
import {
ChevronLeftIcon,
ChevronRightIcon,
FilterIcon,
SortDescendingIcon,
} from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import LoadingSpinner from '../Common/LoadingSpinner';
import RequestItem from './RequestItem';
import Header from '../Common/Header';
import Table from '../Common/Table';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import RequestItem from './RequestItem';
const messages = defineMessages({
requests: 'Requests',
mediaInfo: 'Media Info',
status: 'Status',
requestedAt: 'Requested At',
modifiedBy: 'Last Modified By',
showingresults:
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
resultsperpage: 'Display {pageSize} results per page',
next: 'Next',
previous: 'Previous',
filterAll: 'All',
filterPending: 'Pending',
filterApproved: 'Approved',
filterAvailable: 'Available',
filterProcessing: 'Processing',
noresults: 'No results.',
showallrequests: 'Show All Requests',
sortAdded: 'Request Date',
sortAdded: 'Most Recent',
sortModified: 'Last Modified',
});
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
enum Filter {
ALL = 'all',
PENDING = 'pending',
APPROVED = 'approved',
PROCESSING = 'processing',
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
}
type Sort = 'added' | 'modified';
const RequestList: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const [pageIndex, setPageIndex] = useState(0);
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
const { user } = useUser({
id: Number(router.query.userId),
});
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
const [currentSort, setCurrentSort] = useState<Sort>('added');
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
const page = router.query.page ? Number(router.query.page) : 1;
const pageIndex = page - 1;
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
const {
data,
error,
mutate: revalidate,
} = useSWR<RequestResultsResponse>(
`/api/v1/request?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
}&filter=${currentFilter}&sort=${currentSort}`
}&filter=${currentFilter}&sort=${currentSort}${
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
}`
);
// Restore last set filter values on component mount
useEffect(() => {
const filterString = window.localStorage.getItem('rl-filter-settings');
if (filterString) {
const filterSettings = JSON.parse(filterString);
setCurrentFilter(filterSettings.currentFilter);
setCurrentSort(filterSettings.currentSort);
setCurrentPageSize(filterSettings.currentPageSize);
}
// If filter value is provided in query, use that instead
if (Object.values(Filter).includes(router.query.filter as Filter)) {
setCurrentFilter(router.query.filter as Filter);
}
}, [router.query.filter]);
// Set filter values to local storage any time they are changed
useEffect(() => {
window.localStorage.setItem(
'rl-filter-settings',
JSON.stringify({
currentFilter,
currentSort,
currentPageSize,
})
);
}, [currentFilter, currentSort, currentPageSize]);
if (!data && !error) {
return <LoadingSpinner />;
}
@@ -59,73 +106,81 @@ const RequestList: React.FC = () => {
return (
<>
<PageTitle title={intl.formatMessage(messages.requests)} />
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
<Header>{intl.formatMessage(messages.requests)}</Header>
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
clipRule="evenodd"
/>
</svg>
<PageTitle
title={[
intl.formatMessage(messages.requests),
router.query.userId ? user?.displayName : '',
]}
/>
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
<Header
subtext={
router.query.userId ? (
<Link href={`/users/${user?.id}`}>
<a className="hover:underline">{user?.displayName}</a>
</Link>
) : (
''
)
}
>
{intl.formatMessage(messages.requests)}
</Header>
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
<FilterIcon className="h-6 w-6" />
</span>
<select
id="filter"
name="filter"
onChange={(e) => {
setPageIndex(0);
setCurrentFilter(e.target.value as Filter);
router.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
});
}}
value={currentFilter}
className="rounded-r-only"
>
<option value="all">
{intl.formatMessage(messages.filterAll)}
{intl.formatMessage(globalMessages.all)}
</option>
<option value="pending">
{intl.formatMessage(messages.filterPending)}
{intl.formatMessage(globalMessages.pending)}
</option>
<option value="approved">
{intl.formatMessage(messages.filterApproved)}
{intl.formatMessage(globalMessages.approved)}
</option>
<option value="processing">
{intl.formatMessage(messages.filterProcessing)}
{intl.formatMessage(globalMessages.processing)}
</option>
<option value="available">
{intl.formatMessage(messages.filterAvailable)}
{intl.formatMessage(globalMessages.available)}
</option>
<option value="unavailable">
{intl.formatMessage(globalMessages.unavailable)}
</option>
</select>
</div>
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
</svg>
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
<SortDescendingIcon className="h-6 w-6" />
</span>
<select
id="sort"
name="sort"
onChange={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
}}
onBlur={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
router.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
});
}}
value={currentSort}
className="rounded-r-only"
@@ -140,114 +195,104 @@ const RequestList: React.FC = () => {
</div>
</div>
</div>
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{data.results.map((request) => {
return (
<RequestItem
request={request}
key={`request-list-${request.id}`}
revalidateList={() => revalidate()}
/>
);
})}
{data.results.map((request) => {
return (
<div className="py-2" key={`request-list-${request.id}`}>
<RequestItem
request={request}
revalidateList={() => revalidate()}
/>
</div>
);
})}
{data.results.length === 0 && (
<tr className="relative h-24 p-2 text-white">
<Table.TD colSpan={6} noPadding>
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
<span className="text-base">
{intl.formatMessage(messages.noresults)}
</span>
{currentFilter !== 'all' && (
<div className="mt-4">
<Button
buttonSize="sm"
buttonType="primary"
onClick={() => setCurrentFilter('all')}
>
{intl.formatMessage(messages.showallrequests)}
</Button>
</div>
)}
</div>
</Table.TD>
</tr>
)}
<tr className="bg-gray-700">
<Table.TD colSpan={6} noPadding>
<nav
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
aria-label="Pagination"
{data.results.length === 0 && (
<div className="flex w-full flex-col items-center justify-center py-24 text-white">
<span className="text-2xl text-gray-400">
{intl.formatMessage(globalMessages.noresults)}
</span>
{currentFilter !== Filter.ALL && (
<div className="mt-4">
<Button
buttonType="primary"
onClick={() => setCurrentFilter(Filter.ALL)}
>
<div className="hidden lg:flex lg:flex-1">
<p className="text-sm">
{data.results.length > 0 &&
intl.formatMessage(messages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
data.results.length < currentPageSize
? pageIndex * currentPageSize + data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
})}
</p>
</div>
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
{intl.formatMessage(messages.resultsperpage, {
pageSize: (
<select
id="pageSize"
name="pageSize"
onChange={(e) => {
setPageIndex(0);
setCurrentPageSize(Number(e.target.value));
}}
value={currentPageSize}
className="inline short"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
),
})}
</span>
</div>
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
<Button
disabled={!hasPrevPage}
onClick={() => setPageIndex((current) => current - 1)}
{intl.formatMessage(messages.showallrequests)}
</Button>
</div>
)}
</div>
)}
<div className="actions">
<nav
className="mb-3 flex flex-col items-center space-y-3 sm:flex-row sm:space-y-0"
aria-label="Pagination"
>
<div className="hidden lg:flex lg:flex-1">
<p className="text-sm">
{data.results.length > 0 &&
intl.formatMessage(globalMessages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
data.results.length < currentPageSize
? pageIndex * currentPageSize + data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
})}
</p>
</div>
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
<span className="-mt-3 items-center truncate text-sm sm:mt-0">
{intl.formatMessage(globalMessages.resultsperpage, {
pageSize: (
<select
id="pageSize"
name="pageSize"
onChange={(e) => {
setCurrentPageSize(Number(e.target.value));
router
.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
})
.then(() => window.scrollTo(0, 0));
}}
value={currentPageSize}
className="short inline"
>
{intl.formatMessage(messages.previous)}
</Button>
<Button
disabled={!hasNextPage}
onClick={() => setPageIndex((current) => current + 1)}
>
{intl.formatMessage(messages.next)}
</Button>
</div>
</nav>
</Table.TD>
</tr>
</Table.TBody>
</Table>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
),
})}
</span>
</div>
<div className="flex flex-auto justify-center space-x-2 sm:flex-1 sm:justify-end">
<Button
disabled={!hasPrevPage}
onClick={() => updateQueryParams('page', (page - 1).toString())}
>
<ChevronLeftIcon />
<span>{intl.formatMessage(globalMessages.previous)}</span>
</Button>
<Button
disabled={!hasNextPage}
onClick={() => updateQueryParams('page', (page + 1).toString())}
>
<span>{intl.formatMessage(globalMessages.next)}</span>
<ChevronRightIcon />
</Button>
</div>
</nav>
</div>
</>
);
};

View File

@@ -23,12 +23,14 @@ const messages = defineMessages({
requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K',
edit: 'Edit Request',
approve: 'Approve Request',
cancel: 'Cancel Request',
pendingrequest: 'Pending Request for {title}',
pending4krequest: 'Pending 4K Request for {title}',
requestfrom: "{username}'s request is pending approval.",
errorediting: 'Something went wrong while editing the request.',
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
requestApproved: 'Request for <strong>{title}</strong> approved!',
requesterror: 'Something went wrong while submitting the request.',
pendingapproval: 'Your request is pending approval.',
});
@@ -60,7 +62,10 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
const intl = useIntl();
const { user, hasPermission } = useUser();
const { data: quota } = useSWR<QuotaResponse>(
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
user &&
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
: null
);
useEffect(() => {
@@ -156,7 +161,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
}
};
const updateRequest = async () => {
const updateRequest = async (alsoApproveRequest = false) => {
setIsUpdating(true);
try {
@@ -169,14 +174,23 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
tags: requestOverrides?.tags,
});
if (alsoApproveRequest) {
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
}
addToast(
<span>
{intl.formatMessage(messages.requestedited, {
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})}
{intl.formatMessage(
alsoApproveRequest
? messages.requestApproved
: messages.requestedited,
{
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
}
)}
</span>,
{
appearance: 'success',
@@ -199,12 +213,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
if (editRequest) {
const isOwner = editRequest.requestedBy.id === user?.id;
const showEditButton = hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED],
{
type: 'or',
}
);
return (
<Modal
@@ -215,20 +223,44 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
is4k ? messages.pending4krequest : messages.pendingrequest,
{ title: data?.title }
)}
onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
onOk={() =>
hasPermission(Permission.MANAGE_REQUESTS)
? updateRequest(true)
: hasPermission(Permission.REQUEST_ADVANCED)
? updateRequest()
: cancelRequest()
}
okDisabled={isUpdating}
okText={
showEditButton
hasPermission(Permission.MANAGE_REQUESTS)
? intl.formatMessage(messages.approve)
: hasPermission(Permission.REQUEST_ADVANCED)
? intl.formatMessage(messages.edit)
: intl.formatMessage(messages.cancel)
}
okButtonType={showEditButton ? 'primary' : 'danger'}
okButtonType={
hasPermission(Permission.MANAGE_REQUESTS)
? 'success'
: hasPermission(Permission.REQUEST_ADVANCED)
? 'primary'
: 'danger'
}
onSecondary={
isOwner && showEditButton ? () => cancelRequest() : undefined
isOwner &&
hasPermission(
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
{ type: 'or' }
)
? () => cancelRequest()
: undefined
}
secondaryDisabled={isUpdating}
secondaryText={
isOwner && showEditButton
isOwner &&
hasPermission(
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
{ type: 'or' }
)
? intl.formatMessage(messages.cancel)
: undefined
}
@@ -244,22 +276,20 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
})}
{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<div className="mt-4">
<AdvancedRequester
type="movie"
is4k={is4k}
requestUser={editRequest.requestedBy}
defaultOverrides={{
folder: editRequest.rootFolder,
profile: editRequest.profileId,
server: editRequest.serverId,
tags: editRequest.tags,
}}
onChange={(overrides) => {
setRequestOverrides(overrides);
}}
/>
</div>
<AdvancedRequester
type="movie"
is4k={is4k}
requestUser={editRequest.requestedBy}
defaultOverrides={{
folder: editRequest.rootFolder,
profile: editRequest.profileId,
server: editRequest.serverId,
tags: editRequest.tags,
}}
onChange={(overrides) => {
setRequestOverrides(overrides);
}}
/>
)}
</Modal>
);

View File

@@ -30,13 +30,15 @@ const messages = defineMessages({
requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K',
edit: 'Edit Request',
approve: 'Approve Request',
cancel: 'Cancel Request',
pendingrequest: 'Pending Request for {title}',
pending4krequest: 'Pending 4K Request for {title}',
requestfrom: "{username}'s request is pending approval.",
requestseasons:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
requestall: 'Request All Seasons',
requestseasons4k:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K',
alreadyrequested: 'Already Requested',
selectseason: 'Select Season(s)',
season: 'Season',
@@ -45,6 +47,7 @@ const messages = defineMessages({
extras: 'Extras',
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.',
@@ -88,7 +91,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
});
const [tvdbId, setTvdbId] = useState<number | undefined>(undefined);
const { data: quota } = useSWR<QuotaResponse>(
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
user &&
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
: null
);
const currentlyRemaining =
@@ -96,7 +102,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
selectedSeasons.length +
(editRequest?.seasons ?? []).length;
const updateRequest = async () => {
const updateRequest = async (alsoApproveRequest = false) => {
if (!editRequest) {
return;
}
@@ -117,6 +123,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
tags: requestOverrides?.tags,
seasons: selectedSeasons,
});
if (alsoApproveRequest) {
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
}
} else {
await axios.delete(`/api/v1/request/${editRequest.id}`);
}
@@ -124,12 +134,17 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
addToast(
<span>
{selectedSeasons.length > 0
? intl.formatMessage(messages.requestedited, {
title: data?.name,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})
? intl.formatMessage(
alsoApproveRequest
? messages.requestApproved
: messages.requestedited,
{
title: data?.name,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
}
)
: intl.formatMessage(messages.requestcancelled, {
title: data?.name,
strong: function strong(msg) {
@@ -368,7 +383,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
loading={!data && !error}
backgroundClickable
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
onOk={() => (editRequest ? updateRequest() : sendRequest())}
onOk={() =>
editRequest
? hasPermission(Permission.MANAGE_REQUESTS)
? updateRequest(true)
: updateRequest()
: sendRequest()
}
title={intl.formatMessage(
editRequest
? is4k
@@ -383,16 +404,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
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(messages.requestall)
? intl.formatMessage(
is4k ? globalMessages.request4k : globalMessages.request
)
: selectedSeasons.length === 0
? intl.formatMessage(messages.selectseason)
: intl.formatMessage(messages.requestseasons, {
seasonCount: selectedSeasons.length,
})
: intl.formatMessage(
is4k ? messages.requestseasons4k : messages.requestseasons,
{
seasonCount: selectedSeasons.length,
}
)
}
okDisabled={
editRequest
@@ -406,11 +434,14 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
selectedSeasons.length === 0)
}
okButtonType={
editRequest &&
settings.currentSettings.partialRequestsEnabled &&
selectedSeasons.length === 0
? 'danger'
: `primary`
editRequest
? settings.currentSettings.partialRequestsEnabled &&
selectedSeasons.length === 0
? 'danger'
: hasPermission(Permission.MANAGE_REQUESTS)
? 'success'
: 'primary'
: 'primary'
}
cancelText={
editRequest
@@ -440,7 +471,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
!(
quota?.tv.limit &&
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
) &&
getAllRequestedSeasons().length < getAllSeasons().length &&
!editRequest && (
@@ -457,7 +488,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
quota={quota?.tv}
remaining={
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
? 0
: currentlyRemaining
}
@@ -468,7 +499,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
}
overLimit={
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
? unrequestedSeasons.length
: undefined
}
@@ -482,7 +513,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
<thead>
<tr>
<th
className={`w-16 px-4 py-3 bg-gray-500 ${
className={`w-16 bg-gray-500 px-4 py-3 ${
!settings.currentSettings.partialRequestsEnabled &&
'hidden'
}`}
@@ -497,7 +528,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
toggleAllSeasons();
}
}}
className={`relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none ${
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
@@ -509,28 +540,28 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
aria-hidden="true"
className={`${
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
} 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 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
} absolute left-0 inline-block h-5 w-5 transform 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="px-1 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
<th className="bg-gray-500 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="px-5 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
<th className="bg-gray-500 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="px-2 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
<th className="bg-gray-500 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="bg-gray-600 divide-y divide-gray-700">
<tbody className="divide-y divide-gray-700 bg-gray-600">
{data?.seasons
.filter((season) => season.seasonNumber !== 0)
.map((season) => {
@@ -546,7 +577,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
return (
<tr key={`season-${season.id}`}>
<td
className={`px-4 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap ${
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
!settings.currentSettings
.partialRequestsEnabled && 'hidden'
}`}
@@ -568,7 +599,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
toggleSeason(season.seasonNumber);
}
}}
className={`pt-2 relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
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 &&
@@ -590,7 +621,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
isSelectedSeason(season.seasonNumber)
? 'bg-indigo-500'
: 'bg-gray-800'
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
></span>
<span
aria-hidden="true"
@@ -603,21 +634,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
isSelectedSeason(season.seasonNumber)
? 'translate-x-5'
: 'translate-x-0'
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
} absolute left-0 inline-block h-5 w-5 transform 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="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
<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(messages.extras)
: intl.formatMessage(messages.seasonnumber, {
number: season.seasonNumber,
})}
</td>
<td className="px-5 py-4 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
<td className="whitespace-nowrap px-5 py-4 text-sm leading-5 text-gray-200 md:px-6">
{season.episodeCount}
</td>
<td className="py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
{!seasonRequest && !mediaSeason && (
<Badge>
{intl.formatMessage(
@@ -667,28 +698,26 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
</div>
{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<div className="mt-4">
<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
}
/>
</div>
<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>
);

View File

@@ -1,9 +1,9 @@
import React from 'react';
import MovieRequestModal from './MovieRequestModal';
import type { MediaStatus } from '../../../server/constants/media';
import TvRequestModal from './TvRequestModal';
import Transition from '../Transition';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import Transition from '../Transition';
import MovieRequestModal from './MovieRequestModal';
import TvRequestModal from './TvRequestModal';
interface RequestModalProps {
show: boolean;
@@ -26,29 +26,6 @@ const RequestModal: React.FC<RequestModalProps> = ({
onUpdating,
onCancel,
}) => {
if (type === 'tv') {
return (
<Transition
enter="transition opacity-0 duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={show}
>
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
is4k={is4k}
editRequest={editRequest}
/>
</Transition>
);
}
return (
<Transition
enter="transition opacity-0 duration-300"
@@ -59,14 +36,25 @@ const RequestModal: React.FC<RequestModalProps> = ({
leaveTo="opacity-0"
show={show}
>
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
is4k={is4k}
editRequest={editRequest}
/>
{type === 'movie' ? (
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
is4k={is4k}
editRequest={editRequest}
/>
) : (
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
is4k={is4k}
editRequest={editRequest}
/>
)}
</Transition>
);
};

View File

@@ -13,8 +13,11 @@ interface StatusBadgeProps {
status?: MediaStatus;
is4k?: boolean;
inProgress?: boolean;
plexUrl?: string;
tmdbId?: number;
mediaUrl?: string;
mediaUrl4k?: string;
mediaType?: 'movie' | 'tv';
}
const StatusBadge: React.FC<StatusBadgeProps> = ({