mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 04:39:14 -05:00
Merge remote-tracking branch 'overseerr/develop' into develop
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilIcon,
|
||||
@@ -5,28 +13,17 @@ import {
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
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 Badge from '../../Common/Badge';
|
||||
import Button from '../../Common/Button';
|
||||
import CachedImage from '../../Common/CachedImage';
|
||||
import ConfirmButton from '../../Common/ConfirmButton';
|
||||
import RequestModal from '../../RequestModal';
|
||||
import StatusBadge from '../../StatusBadge';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
@@ -35,50 +32,227 @@ const messages = defineMessages({
|
||||
requesteddate: 'Requested',
|
||||
modified: 'Modified',
|
||||
modifieduserdate: '{date} by {user}',
|
||||
mediaerror: 'The associated title for this request is no longer available.',
|
||||
mediaerror: '{mediaType} Not Found',
|
||||
editrequest: 'Edit Request',
|
||||
deleterequest: 'Delete Request',
|
||||
cancelRequest: 'Cancel Request',
|
||||
tmdbid: 'TMDB ID',
|
||||
tvdbid: 'TheTVDB ID',
|
||||
});
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
return (movie as MovieDetails).title !== undefined;
|
||||
};
|
||||
|
||||
interface RequestItemErroProps {
|
||||
mediaId?: number;
|
||||
interface RequestItemErrorProps {
|
||||
requestData?: MediaRequest;
|
||||
revalidateList: () => void;
|
||||
}
|
||||
|
||||
const RequestItemError: React.FC<RequestItemErroProps> = ({
|
||||
mediaId,
|
||||
const RequestItemError = ({
|
||||
requestData,
|
||||
revalidateList,
|
||||
}) => {
|
||||
}: RequestItemErrorProps) => {
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
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">
|
||||
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
|
||||
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
<div className="flex w-full flex-col justify-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
|
||||
<div className="flex text-lg font-bold text-white xl:text-xl">
|
||||
{intl.formatMessage(messages.mediaerror, {
|
||||
mediaType: intl.formatMessage(
|
||||
requestData?.type
|
||||
? requestData?.type === 'movie'
|
||||
? globalMessages.movie
|
||||
: globalMessages.tvshow
|
||||
: globalMessages.request
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
{requestData && hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.tmdbid)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{requestData.media.tmdbId}
|
||||
</span>
|
||||
</div>
|
||||
{requestData.media.tvdbId && (
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.tvdbid)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{requestData?.media.tvdbId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="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">
|
||||
{requestData && (
|
||||
<>
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</span>
|
||||
{requestData.status === MediaRequestStatus.DECLINED ||
|
||||
requestData.status === MediaRequestStatus.FAILED ? (
|
||||
<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}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
: requestData.media.serviceUrl
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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 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 group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && requestData?.media.id && (
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -88,10 +262,7 @@ interface RequestItemProps {
|
||||
revalidateList: () => void;
|
||||
}
|
||||
|
||||
const RequestItem: React.FC<RequestItemProps> = ({
|
||||
request,
|
||||
revalidateList,
|
||||
}) => {
|
||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
@@ -157,7 +328,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
if (!title || !requestData) {
|
||||
return (
|
||||
<RequestItemError
|
||||
mediaId={requestData?.media.id}
|
||||
requestData={requestData}
|
||||
revalidateList={revalidateList}
|
||||
/>
|
||||
);
|
||||
@@ -276,9 +447,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.declined)}
|
||||
</Badge>
|
||||
) : requestData.media[
|
||||
requestData.is4k ? 'status4k' : 'status'
|
||||
] === MediaStatus.UNKNOWN ? (
|
||||
) : requestData.status === MediaRequestStatus.FAILED ? (
|
||||
<Badge
|
||||
badgeType="danger"
|
||||
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
@@ -301,9 +470,14 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.media[
|
||||
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
|
||||
]
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
: requestData.media.serviceUrl
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -405,9 +579,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<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 &&
|
||||
{requestData.status === MediaRequestStatus.FAILED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
className="w-full"
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import RequestItem from '@app/components/RequestList/RequestItem';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FilterIcon,
|
||||
SortDescendingIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
|
||||
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
|
||||
import { useUser } from '../../hooks/useUser';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Button from '../Common/Button';
|
||||
import Header from '../Common/Header';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import RequestItem from './RequestItem';
|
||||
|
||||
const messages = defineMessages({
|
||||
requests: 'Requests',
|
||||
@@ -33,16 +33,18 @@ enum Filter {
|
||||
PROCESSING = 'processing',
|
||||
AVAILABLE = 'available',
|
||||
UNAVAILABLE = 'unavailable',
|
||||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
type Sort = 'added' | 'modified';
|
||||
|
||||
const RequestList: React.FC = () => {
|
||||
const RequestList = () => {
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
const { user } = useUser({
|
||||
id: Number(router.query.userId),
|
||||
});
|
||||
const { user: currentUser } = useUser();
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
@@ -59,7 +61,11 @@ const RequestList: React.FC = () => {
|
||||
`/api/v1/request?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}&sort=${currentSort}${
|
||||
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
|
||||
router.pathname.startsWith('/profile')
|
||||
? `&requestedBy=${currentUser?.id}`
|
||||
: router.query.userId
|
||||
? `&requestedBy=${router.query.userId}`
|
||||
: ''
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -115,7 +121,11 @@ const RequestList: React.FC = () => {
|
||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
|
||||
<Header
|
||||
subtext={
|
||||
router.query.userId ? (
|
||||
router.pathname.startsWith('/profile') ? (
|
||||
<Link href={`/profile`}>
|
||||
<a className="hover:underline">{currentUser?.displayName}</a>
|
||||
</Link>
|
||||
) : router.query.userId ? (
|
||||
<Link href={`/users/${user?.id}`}>
|
||||
<a className="hover:underline">{user?.displayName}</a>
|
||||
</Link>
|
||||
@@ -158,6 +168,9 @@ const RequestList: React.FC = () => {
|
||||
<option value="processing">
|
||||
{intl.formatMessage(globalMessages.processing)}
|
||||
</option>
|
||||
<option value="failed">
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</option>
|
||||
<option value="available">
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</option>
|
||||
@@ -238,9 +251,9 @@ const RequestList: React.FC = () => {
|
||||
? pageIndex * currentPageSize + data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
strong: (msg: React.ReactNode) => (
|
||||
<span className="font-medium">{msg}</span>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user