feat: 4K Requests (#559)

This commit is contained in:
sct
2021-01-11 23:42:33 +09:00
committed by GitHub
parent 79629645aa
commit 6b2df24a2e
30 changed files with 1384 additions and 467 deletions

View File

@@ -0,0 +1,582 @@
import axios from 'axios';
import React, { useContext, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
MediaRequestStatus,
MediaStatus,
} from '../../../../server/constants/media';
import Media from '../../../../server/entity/Media';
import { MediaRequest } from '../../../../server/entity/MediaRequest';
import { SettingsContext } from '../../../context/SettingsContext';
import { Permission, useUser } from '../../../hooks/useUser';
import ButtonWithDropdown from '../../Common/ButtonWithDropdown';
import RequestModal from '../../RequestModal';
const messages = defineMessages({
viewrequest: 'View Request',
viewrequest4k: 'View 4K Request',
request: 'Request',
request4k: 'Request 4K',
requestmore: 'Request More',
requestmore4k: 'Request More 4K',
approverequest: 'Approve Request',
approverequest4k: 'Approve 4K Request',
declinerequest: 'Decline Request',
declinerequest4k: 'Decline 4K Request',
approverequests:
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}',
declinerequests:
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}',
approve4krequests:
'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}',
decline4krequests:
'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}',
});
interface ButtonOption {
id: string;
text: string;
action: () => void;
svg?: React.ReactNode;
}
interface RequestButtonProps {
mediaType: 'movie' | 'tv';
onUpdate: () => void;
tmdbId: number;
media?: Media;
isShowComplete?: boolean;
is4kShowComplete?: boolean;
}
const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId,
onUpdate,
media,
mediaType,
isShowComplete = false,
is4kShowComplete = false,
}) => {
const intl = useIntl();
const settings = useContext(SettingsContext);
const { hasPermission } = useUser();
const [showRequestModal, setShowRequestModal] = useState(false);
const [showRequest4kModal, setShowRequest4kModal] = useState(false);
const activeRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
// All pending
const activeRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
const modifyRequest = async (
request: MediaRequest,
type: 'approve' | 'decline'
) => {
const response = await axios.get(`/api/v1/request/${request.id}/${type}`);
if (response) {
onUpdate();
}
};
const modifyRequests = async (
requests: MediaRequest[],
type: 'approve' | 'decline'
): Promise<void> => {
if (!requests) {
return;
}
await Promise.all(
requests.map(async (request) => {
return axios.get(`/api/v1/request/${request.id}/${type}`);
})
);
onUpdate();
};
const buttons: ButtonOption[] = [];
if (
(!media || media.status === MediaStatus.UNKNOWN) &&
hasPermission(Permission.REQUEST)
) {
buttons.push({
id: 'request',
text: intl.formatMessage(messages.request),
action: () => {
setShowRequestModal(true);
},
svg: (
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
});
}
if (
hasPermission(Permission.REQUEST) &&
mediaType === 'tv' &&
media &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.UNKNOWN &&
!isShowComplete
) {
buttons.push({
id: 'request-more',
text: intl.formatMessage(messages.requestmore),
action: () => {
setShowRequestModal(true);
},
svg: (
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
});
}
if (
(!media || media.status4k === MediaStatus.UNKNOWN) &&
(hasPermission(Permission.REQUEST_4K) ||
(mediaType === 'movie' && hasPermission(Permission.REQUEST_4K_MOVIE)) ||
(mediaType === 'tv' && hasPermission(Permission.REQUEST_4K_TV))) &&
((settings.currentSettings.movie4kEnabled && mediaType === 'movie') ||
(settings.currentSettings.series4kEnabled && mediaType === 'tv'))
) {
buttons.push({
id: 'request4k',
text: intl.formatMessage(messages.request4k),
action: () => {
setShowRequest4kModal(true);
},
svg: (
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
});
}
if (
mediaType === 'tv' &&
(hasPermission(Permission.REQUEST_4K) ||
(mediaType === 'tv' && hasPermission(Permission.REQUEST_4K_TV))) &&
media &&
media.status4k !== MediaStatus.AVAILABLE &&
media.status4k !== MediaStatus.UNKNOWN &&
!is4kShowComplete &&
settings.currentSettings.series4kEnabled
) {
buttons.push({
id: 'request-more-4k',
text: intl.formatMessage(messages.requestmore4k),
action: () => {
setShowRequest4kModal(true);
},
svg: (
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
});
}
if (
activeRequest &&
mediaType === 'movie' &&
hasPermission(Permission.REQUEST)
) {
buttons.push({
id: 'active-request',
text: intl.formatMessage(messages.viewrequest),
action: () => setShowRequestModal(true),
svg: (
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
});
}
if (
active4kRequest &&
mediaType === 'movie' &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
) {
buttons.push({
id: 'active-4k-request',
text: intl.formatMessage(messages.viewrequest4k),
action: () => setShowRequest4kModal(true),
svg: (
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
});
}
if (
activeRequest &&
hasPermission(Permission.MANAGE_REQUESTS) &&
mediaType === 'movie'
) {
buttons.push(
{
id: 'approve-request',
text: intl.formatMessage(messages.approverequest),
action: () => {
modifyRequest(activeRequest, 'approve');
},
svg: (
<svg
className="w-4 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>
),
},
{
id: 'decline-request',
text: intl.formatMessage(messages.declinerequest),
action: () => {
modifyRequest(activeRequest, 'decline');
},
svg: (
<svg
className="w-4 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>
),
}
);
}
if (
activeRequests &&
activeRequests.length > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) &&
mediaType === 'tv'
) {
buttons.push(
{
id: 'approve-request-batch',
text: intl.formatMessage(messages.approverequests, {
requestCount: activeRequests.length,
}),
action: () => {
modifyRequests(activeRequests, 'approve');
},
svg: (
<svg
className="w-4 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>
),
},
{
id: 'decline-request-batch',
text: intl.formatMessage(messages.declinerequests, {
requestCount: activeRequests.length,
}),
action: () => {
modifyRequests(activeRequests, 'decline');
},
svg: (
<svg
className="w-4 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>
),
}
);
}
if (
active4kRequest &&
hasPermission(Permission.MANAGE_REQUESTS) &&
mediaType === 'movie'
) {
buttons.push(
{
id: 'approve-4k-request',
text: intl.formatMessage(messages.approverequest4k),
action: () => {
modifyRequest(active4kRequest, 'approve');
},
svg: (
<svg
className="w-4 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>
),
},
{
id: 'decline-4k-request',
text: intl.formatMessage(messages.declinerequest4k),
action: () => {
modifyRequest(active4kRequest, 'decline');
},
svg: (
<svg
className="w-4 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>
),
}
);
}
if (
active4kRequests &&
active4kRequests.length > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) &&
mediaType === 'tv'
) {
buttons.push(
{
id: 'approve-request-batch',
text: intl.formatMessage(messages.approve4krequests, {
requestCount: active4kRequests.length,
}),
action: () => {
modifyRequests(active4kRequests, 'approve');
},
svg: (
<svg
className="w-4 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>
),
},
{
id: 'decline-request-batch',
text: intl.formatMessage(messages.decline4krequests, {
requestCount: active4kRequests.length,
}),
action: () => {
modifyRequests(active4kRequests, 'decline');
},
svg: (
<svg
className="w-4 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>
),
}
);
}
const [buttonOne, ...others] = buttons;
if (!buttonOne) {
return null;
}
return (
<>
<RequestModal
tmdbId={tmdbId}
show={showRequestModal}
type={mediaType}
onComplete={() => {
onUpdate();
setShowRequestModal(false);
}}
onCancel={() => setShowRequestModal(false)}
/>
<RequestModal
tmdbId={tmdbId}
show={showRequest4kModal}
type={mediaType}
is4k
onComplete={() => {
onUpdate();
setShowRequest4kModal(false);
}}
onCancel={() => setShowRequest4kModal(false)}
/>
<ButtonWithDropdown
text={
<>
{buttonOne.svg ?? null}
{buttonOne.text}
</>
}
onClick={buttonOne.action}
className="ml-2"
>
{others && others.length > 0
? others.map((button) => (
<ButtonWithDropdown.Item
onClick={button.action}
key={`request-option-${button.id}`}
>
{button.svg}
{button.text}
</ButtonWithDropdown.Item>
))
: null}
{/* {hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<ButtonWithDropdown.Item onClick={() => modifyRequest('approve')}>
<svg
className="w-4 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>
{intl.formatMessage(messages.approve)}
</ButtonWithDropdown.Item>
</>
)} */}
</ButtonWithDropdown>
</>
);
};
export default RequestButton;

View File

@@ -18,12 +18,7 @@ import PersonCard from '../PersonCard';
import { LanguageContext } from '../../context/LanguageContext';
import LoadingSpinner from '../Common/LoadingSpinner';
import { useUser, Permission } from '../../hooks/useUser';
import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import { MediaStatus } from '../../../server/constants/media';
import axios from 'axios';
import SlideOver from '../Common/SlideOver';
import RequestBlock from '../RequestBlock';
@@ -38,6 +33,7 @@ import Head from 'next/head';
import ExternalLinkBlock from '../ExternalLinkBlock';
import { sortCrewPriority } from '../../utils/creditHelpers';
import StatusBadge from '../StatusBadge';
import RequestButton from './RequestButton';
const messages = defineMessages({
releasedate: 'Release Date',
@@ -55,8 +51,6 @@ const messages = defineMessages({
cancelrequest: 'Cancel Request',
available: 'Available',
unavailable: 'Unavailable',
request: 'Request',
viewrequest: 'View Request',
pending: 'Pending',
overviewunavailable: 'Overview unavailable',
manageModalTitle: 'Manage Movie',
@@ -88,7 +82,6 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const [showRequestModal, setShowRequestModal] = useState(false);
const [showManager, setShowManager] = useState(false);
const { data, error, revalidate } = useSWR<MovieDetailsType>(
`/api/v1/movie/${router.query.movieId}?language=${locale}`,
@@ -118,25 +111,11 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
return <Error statusCode={404} />;
}
const activeRequest = data?.mediaInfo?.requests?.find(
(request) => request.status === MediaRequestStatus.PENDING
);
const trailerUrl = data.relatedVideos
?.filter((r) => r.type === 'Trailer')
.sort((a, b) => a.size - b.size)
.pop()?.url;
const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.get(
`/api/v1/request/${activeRequest?.id}/${type}`
);
if (response) {
revalidate();
}
};
const deleteMedia = async () => {
if (data?.mediaInfo?.id) {
await axios.delete(`/api/v1/media/${data?.mediaInfo?.id}`);
@@ -155,16 +134,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<Head>
<title>{data.title} - Overseerr</title>
</Head>
<RequestModal
tmdbId={data.id}
show={showRequestModal}
type="movie"
onComplete={() => {
revalidate();
setShowRequestModal(false);
}}
onCancel={() => setShowRequestModal(false)}
/>
<SlideOver
show={showManager}
title={intl.formatMessage(messages.manageModalTitle)}
@@ -216,7 +186,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div>
<div className="flex flex-col flex-1 mt-4 text-center text-white lg:mr-4 lg:mt-0 lg:text-left">
<div className="mb-2">
<StatusBadge status={data.mediaInfo?.status} />
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && (
<span className="mr-2">
<StatusBadge status={data.mediaInfo?.status} />
</span>
)}
<span>
<StatusBadge status={data.mediaInfo?.status4k} is4k />
</span>
</div>
<h1 className="text-2xl lg:text-4xl">
{data.title}{' '}
@@ -263,121 +240,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</Button>
</a>
)}
{(!data.mediaInfo ||
data.mediaInfo?.status === MediaStatus.UNKNOWN) && (
<Button
buttonType="primary"
className="ml-2"
onClick={() => setShowRequestModal(true)}
>
{activeRequest ? (
<svg
className="w-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
)}
<FormattedMessage {...messages.request} />
</Button>
)}
{activeRequest && (
<ButtonWithDropdown
dropdownIcon={
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
clipRule="evenodd"
/>
</svg>
}
text={
<>
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
<FormattedMessage {...messages.viewrequest} />
</>
}
onClick={() => setShowRequestModal(true)}
className="ml-2"
>
{hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<ButtonWithDropdown.Item
onClick={() => modifyRequest('approve')}
>
<svg
className="w-4 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>
{intl.formatMessage(messages.approve)}
</ButtonWithDropdown.Item>
<ButtonWithDropdown.Item
onClick={() => modifyRequest('decline')}
>
<svg
className="w-4 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>
{intl.formatMessage(messages.decline)}
</ButtonWithDropdown.Item>
</>
)}
</ButtonWithDropdown>
)}
<RequestButton
mediaType="movie"
media={data.mediaInfo}
tmdbId={data.id}
onUpdate={() => revalidate()}
/>
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"