feat: 4K Requests (#559)
This commit is contained in:
582
src/components/MovieDetails/RequestButton/index.tsx
Normal file
582
src/components/MovieDetails/RequestButton/index.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -45,8 +45,8 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
<div className="block">
|
||||
<div className="px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-6 flex-col items-center text-sm leading-5 text-gray-300 flex-1 min-w-0">
|
||||
<div className="flex flex-nowrap mb-1 white">
|
||||
<div className="flex-col items-center flex-1 min-w-0 mr-6 text-sm leading-5 text-gray-300">
|
||||
<div className="flex mb-1 flex-nowrap white">
|
||||
<svg
|
||||
className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
|
||||
fill="currentColor"
|
||||
@@ -59,7 +59,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="truncate w-40 md:w-auto">
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
{request.requestedBy.username}
|
||||
</span>
|
||||
</div>
|
||||
@@ -78,13 +78,13 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="truncate w-40 md:w-auto">
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
{request.modifiedBy?.username}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-2 flex-shrink-0 flex flex-wrap">
|
||||
<div className="flex flex-wrap flex-shrink-0 ml-2">
|
||||
{request.status === MediaRequestStatus.PENDING && (
|
||||
<>
|
||||
<span className="mr-1">
|
||||
@@ -153,11 +153,11 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
</div>
|
||||
<div className="mt-2 sm:flex sm:justify-between">
|
||||
<div className="sm:flex">
|
||||
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
|
||||
{request.status === MediaRequestStatus.AVAILABLE && (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</Badge>
|
||||
<div className="flex items-center mr-6 text-sm leading-5 text-gray-300">
|
||||
{request.is4k && (
|
||||
<span className="mr-1">
|
||||
<Badge badgeType="warning">4K</Badge>
|
||||
</span>
|
||||
)}
|
||||
{request.status === MediaRequestStatus.APPROVED && (
|
||||
<Badge badgeType="success">
|
||||
@@ -176,7 +176,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm leading-5 text-gray-300 sm:mt-0">
|
||||
<div className="flex items-center mt-2 text-sm leading-5 text-gray-300 sm:mt-0">
|
||||
<svg
|
||||
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -195,13 +195,13 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
</div>
|
||||
</div>
|
||||
{(request.seasons ?? []).length > 0 && (
|
||||
<div className="mt-2 text-sm flex flex-col">
|
||||
<div className="flex flex-col mt-2 text-sm">
|
||||
<div className="mb-2">{intl.formatMessage(messages.seasons)}</div>
|
||||
<div>
|
||||
{request.seasons.map((season) => (
|
||||
<span
|
||||
key={`season-${season.id}`}
|
||||
className="mr-2 mb-1 inline-block"
|
||||
className="inline-block mb-1 mr-2"
|
||||
>
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
|
||||
@@ -28,7 +28,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
|
||||
const RequestCardPlaceholder: React.FC = () => {
|
||||
return (
|
||||
<div className="w-72 sm:w-96 relative animate-pulse rounded-lg bg-gray-700 p-4">
|
||||
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
|
||||
<div className="w-20 sm:w-28">
|
||||
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
||||
</div>
|
||||
@@ -88,13 +88,13 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-72 sm:w-96 p-4 bg-gray-800 rounded-md flex bg-cover bg-center text-gray-400"
|
||||
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-1 pr-4 min-w-0 flex flex-col">
|
||||
<h2 className="text-base sm:text-lg overflow-ellipsis overflow-hidden whitespace-nowrap text-white cursor-pointer hover:underline">
|
||||
<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={
|
||||
@@ -106,18 +106,21 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="text-xs sm:text-sm truncate">
|
||||
<div className="text-xs truncate sm:text-sm">
|
||||
{intl.formatMessage(messages.requestedby, {
|
||||
username: requestData.requestedBy.username,
|
||||
})}
|
||||
</div>
|
||||
{requestData.media.status && (
|
||||
<div className="mt-1 sm:mt-2">
|
||||
<StatusBadge status={requestData.media.status} />
|
||||
<StatusBadge
|
||||
status={requestData.media.status}
|
||||
is4k={requestData.is4k}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{request.seasons.length > 0 && (
|
||||
<div className="hidden mt-2 text-sm sm:flex items-center">
|
||||
<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)
|
||||
@@ -126,7 +129,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
|
||||
<Badge>{intl.formatMessage(messages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="hide-scrollbar overflow-x-scroll">
|
||||
<div className="overflow-x-scroll hide-scrollbar">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
@@ -138,7 +141,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="flex-1 flex items-end">
|
||||
<div className="flex items-end flex-1">
|
||||
<span className="mr-2">
|
||||
<Button
|
||||
buttonType="success"
|
||||
@@ -200,7 +203,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`}
|
||||
alt=""
|
||||
className="w-20 sm:w-28 rounded-md shadow-sm cursor-pointer transition transform-gpu duration-300 scale-100 hover:scale-105 hover:shadow-md"
|
||||
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"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -22,17 +22,22 @@ const messages = defineMessages({
|
||||
requestSuccess: '<strong>{title}</strong> successfully requested!',
|
||||
requestCancel: 'Request for <strong>{title}</strong> cancelled',
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
close: 'Close',
|
||||
cancel: 'Cancel Request',
|
||||
cancelling: 'Cancelling...',
|
||||
pendingrequest: 'Pending request for {title}',
|
||||
pending4krequest: 'Pending request for {title} in 4K',
|
||||
requesting: 'Requesting...',
|
||||
request: 'Request',
|
||||
request4k: 'Request 4K',
|
||||
requestfrom: 'There is currently a pending request from {username}',
|
||||
request4kfrom: 'There is currently a pending 4K request from {username}',
|
||||
});
|
||||
|
||||
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
tmdbId: number;
|
||||
is4k?: boolean;
|
||||
onCancel?: () => void;
|
||||
onComplete?: (newStatus: MediaStatus) => void;
|
||||
onUpdating?: (isUpdating: boolean) => void;
|
||||
@@ -43,6 +48,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
onComplete,
|
||||
tmdbId,
|
||||
onUpdating,
|
||||
is4k,
|
||||
}) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const { addToast } = useToasts();
|
||||
@@ -63,6 +69,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
const response = await axios.post<MediaRequest>('/api/v1/request', {
|
||||
mediaId: data?.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
@@ -89,7 +96,9 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
}
|
||||
}, [data, onComplete, addToast]);
|
||||
|
||||
const activeRequest = data?.mediaInfo?.requests?.[0];
|
||||
const activeRequest = data?.mediaInfo?.requests?.find(
|
||||
(request) => request.is4k === !!is4k
|
||||
);
|
||||
|
||||
const cancelRequest = async () => {
|
||||
setIsUpdating(true);
|
||||
@@ -133,9 +142,12 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
onCancel={onCancel}
|
||||
onOk={isOwner ? () => cancelRequest() : undefined}
|
||||
okDisabled={isUpdating}
|
||||
title={intl.formatMessage(messages.pendingrequest, {
|
||||
title: data?.title,
|
||||
})}
|
||||
title={intl.formatMessage(
|
||||
is4k ? messages.pending4krequest : messages.pendingrequest,
|
||||
{
|
||||
title: data?.title,
|
||||
}
|
||||
)}
|
||||
okText={
|
||||
isUpdating
|
||||
? intl.formatMessage(messages.cancelling)
|
||||
@@ -145,9 +157,12 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
cancelText={intl.formatMessage(messages.close)}
|
||||
iconSvg={<DownloadIcon className="w-6 h-6" />}
|
||||
>
|
||||
{intl.formatMessage(messages.requestfrom, {
|
||||
username: activeRequest.requestedBy.username,
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
is4k ? messages.request4kfrom : messages.requestfrom,
|
||||
{
|
||||
username: activeRequest.requestedBy.username,
|
||||
}
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -159,11 +174,14 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
onCancel={onCancel}
|
||||
onOk={sendRequest}
|
||||
okDisabled={isUpdating}
|
||||
title={intl.formatMessage(messages.requesttitle, { title: data?.title })}
|
||||
title={intl.formatMessage(
|
||||
is4k ? messages.request4ktitle : messages.requesttitle,
|
||||
{ title: data?.title }
|
||||
)}
|
||||
okText={
|
||||
isUpdating
|
||||
? intl.formatMessage(messages.requesting)
|
||||
: intl.formatMessage(messages.request)
|
||||
: intl.formatMessage(is4k ? messages.request4k : messages.request)
|
||||
}
|
||||
okButtonType={'primary'}
|
||||
iconSvg={<DownloadIcon className="w-6 h-6" />}
|
||||
|
||||
@@ -23,6 +23,7 @@ const messages = defineMessages({
|
||||
requestSuccess: '<strong>{title}</strong> successfully requested!',
|
||||
requestCancel: 'Request for <strong>{title}</strong> cancelled',
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
requesting: 'Requesting...',
|
||||
requestseasons:
|
||||
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
|
||||
@@ -40,6 +41,7 @@ interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onCancel?: () => void;
|
||||
onComplete?: (newStatus: MediaStatus) => void;
|
||||
onUpdating?: (isUpdating: boolean) => void;
|
||||
is4k?: boolean;
|
||||
}
|
||||
|
||||
const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
@@ -47,6 +49,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
onComplete,
|
||||
tmdbId,
|
||||
onUpdating,
|
||||
is4k = false,
|
||||
}) => {
|
||||
const { addToast } = useToasts();
|
||||
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
|
||||
@@ -65,6 +68,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
mediaId: data?.id,
|
||||
tvdbId: data?.externalIds.tvdbId,
|
||||
mediaType: 'tv',
|
||||
is4k,
|
||||
seasons: selectedSeasons,
|
||||
});
|
||||
|
||||
@@ -90,21 +94,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
};
|
||||
|
||||
const getAllRequestedSeasons = (): number[] => {
|
||||
const requestedSeasons = (data?.mediaInfo?.requests ?? []).reduce(
|
||||
(requestedSeasons, request) => {
|
||||
const requestedSeasons = (data?.mediaInfo?.requests ?? [])
|
||||
.filter((request) => request.is4k === is4k)
|
||||
.reduce((requestedSeasons, request) => {
|
||||
return [
|
||||
...requestedSeasons,
|
||||
...request.seasons.map((sr) => sr.seasonNumber),
|
||||
];
|
||||
},
|
||||
[] as number[]
|
||||
);
|
||||
}, [] as number[]);
|
||||
|
||||
const availableSeasons = (data?.mediaInfo?.seasons ?? [])
|
||||
.filter(
|
||||
(season) =>
|
||||
(season.status === MediaStatus.AVAILABLE ||
|
||||
season.status === MediaStatus.PARTIALLY_AVAILABLE) &&
|
||||
(season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
|
||||
season[is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.PARTIALLY_AVAILABLE) &&
|
||||
!requestedSeasons.includes(season.seasonNumber)
|
||||
)
|
||||
.map((season) => season.seasonNumber);
|
||||
@@ -176,14 +180,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
seasonNumber: number
|
||||
): SeasonRequest | undefined => {
|
||||
let seasonRequest: SeasonRequest | undefined;
|
||||
if (data?.mediaInfo && (data.mediaInfo.requests || []).length > 0) {
|
||||
data.mediaInfo.requests.forEach((request) => {
|
||||
if (!seasonRequest) {
|
||||
seasonRequest = request.seasons.find(
|
||||
(season) => season.seasonNumber === seasonNumber
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
data?.mediaInfo &&
|
||||
(data.mediaInfo.requests || []).filter((request) => request.is4k === is4k)
|
||||
.length > 0
|
||||
) {
|
||||
data.mediaInfo.requests
|
||||
.filter((request) => request.is4k === is4k)
|
||||
.forEach((request) => {
|
||||
if (!seasonRequest) {
|
||||
seasonRequest = request.seasons.find(
|
||||
(season) => season.seasonNumber === seasonNumber
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return seasonRequest;
|
||||
@@ -195,7 +206,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
backgroundClickable
|
||||
onCancel={onCancel}
|
||||
onOk={() => sendRequest()}
|
||||
title={intl.formatMessage(messages.requesttitle, { title: data?.name })}
|
||||
title={intl.formatMessage(
|
||||
is4k ? messages.request4ktitle : messages.requesttitle,
|
||||
{ title: data?.name }
|
||||
)}
|
||||
okText={
|
||||
selectedSeasons.length === 0
|
||||
? intl.formatMessage(messages.selectseason)
|
||||
@@ -256,13 +270,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-1 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
|
||||
<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">
|
||||
{intl.formatMessage(messages.season)}
|
||||
</th>
|
||||
<th className="px-5 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
|
||||
<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">
|
||||
{intl.formatMessage(messages.numberofepisodes)}
|
||||
</th>
|
||||
<th className="px-2 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
|
||||
<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">
|
||||
{intl.formatMessage(messages.status)}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -275,7 +289,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
season.seasonNumber
|
||||
);
|
||||
const mediaSeason = data?.mediaInfo?.seasons.find(
|
||||
(sn) => sn.seasonNumber === season.seasonNumber
|
||||
(sn) =>
|
||||
sn.seasonNumber === season.seasonNumber &&
|
||||
sn[is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.UNKNOWN
|
||||
);
|
||||
return (
|
||||
<tr key={`season-${season.id}`}>
|
||||
@@ -320,17 +337,17 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 md:px-6 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap">
|
||||
<td className="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(messages.extras)
|
||||
: intl.formatMessage(messages.seasonnumber, {
|
||||
number: season.seasonNumber,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-5 md:px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
|
||||
<td className="px-5 py-4 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
{season.episodeCount}
|
||||
</td>
|
||||
<td className="pr-2 md:px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
|
||||
<td className="py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
{!seasonRequest && !mediaSeason && (
|
||||
<Badge>
|
||||
{intl.formatMessage(messages.notrequested)}
|
||||
@@ -357,7 +374,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</Badge>
|
||||
)}
|
||||
{mediaSeason?.status ===
|
||||
{mediaSeason?.[is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.PARTIALLY_AVAILABLE && (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(
|
||||
@@ -365,7 +382,8 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{mediaSeason?.status === MediaStatus.AVAILABLE && (
|
||||
{mediaSeason?.[is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.AVAILABLE && (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</Badge>
|
||||
|
||||
@@ -8,6 +8,7 @@ interface RequestModalProps {
|
||||
show: boolean;
|
||||
type: 'movie' | 'tv';
|
||||
tmdbId: number;
|
||||
is4k?: boolean;
|
||||
onComplete?: (newStatus: MediaStatus) => void;
|
||||
onError?: (error: string) => void;
|
||||
onCancel?: () => void;
|
||||
@@ -18,6 +19,7 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
type,
|
||||
show,
|
||||
tmdbId,
|
||||
is4k,
|
||||
onComplete,
|
||||
onUpdating,
|
||||
onCancel,
|
||||
@@ -38,6 +40,7 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
@@ -58,6 +61,7 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
|
||||
@@ -89,6 +89,30 @@ const SettingsMain: React.FC = () => {
|
||||
description: intl.formatMessage(permissionMessages.requestDescription),
|
||||
permission: Permission.REQUEST,
|
||||
},
|
||||
{
|
||||
id: 'request4k',
|
||||
name: intl.formatMessage(permissionMessages.request4k),
|
||||
description: intl.formatMessage(permissionMessages.request4kDescription),
|
||||
permission: Permission.REQUEST_4K,
|
||||
children: [
|
||||
{
|
||||
id: 'request4k-movies',
|
||||
name: intl.formatMessage(permissionMessages.request4kMovies),
|
||||
description: intl.formatMessage(
|
||||
permissionMessages.request4kMoviesDescription
|
||||
),
|
||||
permission: Permission.REQUEST_4K_MOVIE,
|
||||
},
|
||||
{
|
||||
id: 'request4k-tv',
|
||||
name: intl.formatMessage(permissionMessages.request4kTv),
|
||||
description: intl.formatMessage(
|
||||
permissionMessages.request4kTvDescription
|
||||
),
|
||||
permission: Permission.REQUEST_4K_TV,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'autoapprove',
|
||||
name: intl.formatMessage(permissionMessages.autoapprove),
|
||||
|
||||
@@ -35,7 +35,6 @@ const messages = defineMessages({
|
||||
nodefault: 'No default server selected!',
|
||||
nodefaultdescription:
|
||||
'At least one server must be marked as default before any requests will make it to your services.',
|
||||
no4kimplemented: '(Default 4K servers are not currently implemented)',
|
||||
});
|
||||
|
||||
interface ServerInstanceProps {
|
||||
@@ -63,10 +62,10 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<li className="col-span-1 bg-gray-700 rounded-lg shadow">
|
||||
<div className="w-full flex items-center justify-between p-6 space-x-6">
|
||||
<div className="flex items-center justify-between w-full p-6 space-x-6">
|
||||
<div className="flex-1 truncate">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-white text-sm leading-5 font-medium truncate">
|
||||
<div className="flex items-center mb-2 space-x-3">
|
||||
<h3 className="text-sm font-medium leading-5 text-white truncate">
|
||||
{name}
|
||||
</h3>
|
||||
{isDefault && (
|
||||
@@ -85,31 +84,31 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-gray-300 text-sm leading-5 truncate">
|
||||
<span className="font-bold mr-2">
|
||||
<p className="mt-1 text-sm leading-5 text-gray-300 truncate">
|
||||
<span className="mr-2 font-bold">
|
||||
<FormattedMessage {...messages.address} />
|
||||
</span>
|
||||
{address}
|
||||
</p>
|
||||
<p className="mt-1 text-gray-300 text-sm leading-5 truncate">
|
||||
<span className="font-bold mr-2">
|
||||
<p className="mt-1 text-sm leading-5 text-gray-300 truncate">
|
||||
<span className="mr-2 font-bold">
|
||||
<FormattedMessage {...messages.activeProfile} />
|
||||
</span>{' '}
|
||||
{profileName}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
className="w-10 h-10 flex-shrink-0"
|
||||
className="flex-shrink-0 w-10 h-10"
|
||||
src={`/images/${isSonarr ? 'sonarr' : 'radarr'}_logo.png`}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-800">
|
||||
<div className="-mt-px flex">
|
||||
<div className="w-0 flex-1 flex border-r border-gray-800">
|
||||
<div className="flex -mt-px">
|
||||
<div className="flex flex-1 w-0 border-r border-gray-800">
|
||||
<button
|
||||
onClick={() => onEdit()}
|
||||
className="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 text-gray-200 font-medium border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10 transition ease-in-out duration-150"
|
||||
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 -mr-px text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
@@ -124,10 +123,10 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="-ml-px w-0 flex-1 flex">
|
||||
<div className="flex flex-1 w-0 -ml-px">
|
||||
<button
|
||||
onClick={() => onDelete()}
|
||||
className="relative w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 text-gray-200 font-medium border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10 transition ease-in-out duration-150"
|
||||
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
@@ -200,10 +199,10 @@ const SettingsServices: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-200">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-200">
|
||||
<FormattedMessage {...messages.radarrsettings} />
|
||||
</h3>
|
||||
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
||||
<FormattedMessage {...messages.radarrSettingsDescription} />
|
||||
</p>
|
||||
</div>
|
||||
@@ -262,9 +261,6 @@ const SettingsServices: React.FC = () => {
|
||||
) && (
|
||||
<Alert title={intl.formatMessage(messages.nodefault)}>
|
||||
<p>{intl.formatMessage(messages.nodefaultdescription)}</p>
|
||||
<p className="mt-2">
|
||||
{intl.formatMessage(messages.no4kimplemented)}
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -287,7 +283,7 @@ const SettingsServices: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<li className="h-32 col-span-1 border-2 border-gray-400 border-dashed rounded-lg shadow sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
@@ -316,10 +312,10 @@ const SettingsServices: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-200">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-200">
|
||||
<FormattedMessage {...messages.sonarrsettings} />
|
||||
</h3>
|
||||
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
||||
<FormattedMessage {...messages.sonarrSettingsDescription} />
|
||||
</p>
|
||||
</div>
|
||||
@@ -333,9 +329,6 @@ const SettingsServices: React.FC = () => {
|
||||
) && (
|
||||
<Alert title={intl.formatMessage(messages.nodefault)}>
|
||||
<p>{intl.formatMessage(messages.nodefaultdescription)}</p>
|
||||
<p className="mt-2">
|
||||
{intl.formatMessage(messages.no4kimplemented)}
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -359,7 +352,7 @@ const SettingsServices: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<li className="h-32 col-span-1 border-2 border-gray-400 border-dashed rounded-lg shadow sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
|
||||
@@ -1,16 +1,60 @@
|
||||
import React from 'react';
|
||||
import { MediaStatus } from '../../../server/constants/media';
|
||||
import Badge from '../Common/Badge';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
status4k: '4K {status}',
|
||||
});
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status?: MediaStatus;
|
||||
is4k?: boolean;
|
||||
}
|
||||
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({ status }) => {
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({ status, is4k }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (is4k) {
|
||||
switch (status) {
|
||||
case MediaStatus.AVAILABLE:
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.available),
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
case MediaStatus.PARTIALLY_AVAILABLE:
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.partiallyavailable),
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
case MediaStatus.PROCESSING:
|
||||
return (
|
||||
<Badge badgeType="primary">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.requested),
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
case MediaStatus.PENDING:
|
||||
return (
|
||||
<Badge badgeType="warning">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.pending),
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case MediaStatus.AVAILABLE:
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useUser, Permission } from '../../hooks/useUser';
|
||||
import { TvDetails as TvDetailsType } from '../../../server/models/Tv';
|
||||
import { MediaStatus } from '../../../server/constants/media';
|
||||
import RequestModal from '../RequestModal';
|
||||
import ButtonWithDropdown from '../Common/ButtonWithDropdown';
|
||||
import axios from 'axios';
|
||||
import SlideOver from '../Common/SlideOver';
|
||||
import RequestBlock from '../RequestBlock';
|
||||
@@ -36,6 +35,7 @@ import ExternalLinkBlock from '../ExternalLinkBlock';
|
||||
import { sortCrewPriority } from '../../utils/creditHelpers';
|
||||
import { Crew } from '../../../server/models/common';
|
||||
import StatusBadge from '../StatusBadge';
|
||||
import RequestButton from '../MovieDetails/RequestButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
firstAirDate: 'First Air Date',
|
||||
@@ -50,14 +50,8 @@ const messages = defineMessages({
|
||||
watchtrailer: 'Watch Trailer',
|
||||
available: 'Available',
|
||||
unavailable: 'Unavailable',
|
||||
request: 'Request',
|
||||
requestmore: 'Request More',
|
||||
pending: 'Pending',
|
||||
overviewunavailable: 'Overview unavailable',
|
||||
approverequests:
|
||||
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}',
|
||||
declinerequests:
|
||||
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}',
|
||||
manageModalTitle: 'Manage Series',
|
||||
manageModalRequests: 'Requests',
|
||||
manageModalNoRequests: 'No Requests',
|
||||
@@ -83,13 +77,6 @@ interface SearchResult {
|
||||
results: TvResult[];
|
||||
}
|
||||
|
||||
enum MediaRequestStatus {
|
||||
PENDING = 1,
|
||||
APPROVED,
|
||||
DECLINED,
|
||||
AVAILABLE,
|
||||
}
|
||||
|
||||
const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
const { hasPermission } = useUser();
|
||||
const router = useRouter();
|
||||
@@ -126,29 +113,11 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
return <Error statusCode={404} />;
|
||||
}
|
||||
|
||||
const activeRequests = data.mediaInfo?.requests?.filter(
|
||||
(request) => request.status === MediaRequestStatus.PENDING
|
||||
);
|
||||
|
||||
const trailerUrl = data.relatedVideos
|
||||
?.filter((r) => r.type === 'Trailer')
|
||||
.sort((a, b) => a.size - b.size)
|
||||
.pop()?.url;
|
||||
|
||||
const modifyRequests = async (type: 'approve' | 'decline'): Promise<void> => {
|
||||
if (!activeRequests) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
activeRequests.map(async (request) => {
|
||||
return axios.get(`/api/v1/request/${request.id}/${type}`);
|
||||
})
|
||||
);
|
||||
|
||||
revalidate();
|
||||
};
|
||||
|
||||
const deleteMedia = async () => {
|
||||
if (data?.mediaInfo?.id) {
|
||||
await axios.delete(`/api/v1/media/${data?.mediaInfo?.id}`);
|
||||
@@ -164,6 +133,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
const is4kComplete =
|
||||
data.seasons.filter((season) => season.seasonNumber !== 0).length <=
|
||||
(
|
||||
data.mediaInfo?.seasons.filter(
|
||||
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover"
|
||||
@@ -236,7 +213,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
</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">
|
||||
<span>{data.name}</span>
|
||||
@@ -278,116 +262,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
{(!data.mediaInfo ||
|
||||
data.mediaInfo.status === MediaStatus.UNKNOWN) && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
buttonType="primary"
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
{data.mediaInfo &&
|
||||
data.mediaInfo.status !== MediaStatus.UNKNOWN &&
|
||||
!isComplete && (
|
||||
<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-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>
|
||||
<FormattedMessage {...messages.requestmore} />
|
||||
</>
|
||||
}
|
||||
className="ml-2"
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
activeRequests &&
|
||||
activeRequests.length > 0 && (
|
||||
<>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('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>
|
||||
<FormattedMessage
|
||||
{...messages.approverequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('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>
|
||||
<FormattedMessage
|
||||
{...messages.declinerequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</ButtonWithDropdown>
|
||||
)}
|
||||
<RequestButton
|
||||
mediaType="tv"
|
||||
onUpdate={() => revalidate()}
|
||||
tmdbId={data?.id}
|
||||
media={data?.mediaInfo}
|
||||
isShowComplete={isComplete}
|
||||
is4kShowComplete={is4kComplete}
|
||||
/>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="default"
|
||||
|
||||
@@ -41,6 +41,12 @@ export const messages = defineMessages({
|
||||
autoapproveSeries: 'Auto Approve Series',
|
||||
autoapproveSeriesDescription:
|
||||
'Grants auto approve for series requests made by this user.',
|
||||
request4k: 'Request 4K',
|
||||
request4kDescription: 'Grants permission to request 4K movies and series.',
|
||||
request4kMovies: 'Request 4K Movies',
|
||||
request4kMoviesDescription: 'Grants permission to request 4K movies.',
|
||||
request4kTv: 'Request 4K Series',
|
||||
request4kTvDescription: 'Grants permission to request 4K Series.',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
usersaved: 'User saved',
|
||||
@@ -127,6 +133,26 @@ const UserEdit: React.FC = () => {
|
||||
description: intl.formatMessage(messages.requestDescription),
|
||||
permission: Permission.REQUEST,
|
||||
},
|
||||
{
|
||||
id: 'request4k',
|
||||
name: intl.formatMessage(messages.request4k),
|
||||
description: intl.formatMessage(messages.request4kDescription),
|
||||
permission: Permission.REQUEST_4K,
|
||||
children: [
|
||||
{
|
||||
id: 'request4k-movies',
|
||||
name: intl.formatMessage(messages.request4kMovies),
|
||||
description: intl.formatMessage(messages.request4kMoviesDescription),
|
||||
permission: Permission.REQUEST_4K_MOVIE,
|
||||
},
|
||||
{
|
||||
id: 'request4k-tv',
|
||||
name: intl.formatMessage(messages.request4kTv),
|
||||
description: intl.formatMessage(messages.request4kTvDescription),
|
||||
permission: Permission.REQUEST_4K_TV,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'autoapprove',
|
||||
name: intl.formatMessage(messages.autoapprove),
|
||||
|
||||
39
src/context/SettingsContext.tsx
Normal file
39
src/context/SettingsContext.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
|
||||
import useSWR from 'swr';
|
||||
|
||||
interface SettingsContextProps {
|
||||
currentSettings: PublicSettingsResponse;
|
||||
}
|
||||
|
||||
const defaultSettings = {
|
||||
initialized: false,
|
||||
movie4kEnabled: false,
|
||||
series4kEnabled: false,
|
||||
};
|
||||
|
||||
export const SettingsContext = React.createContext<SettingsContextProps>({
|
||||
currentSettings: defaultSettings,
|
||||
});
|
||||
|
||||
export const SettingsProvider: React.FC<SettingsContextProps> = ({
|
||||
children,
|
||||
currentSettings,
|
||||
}) => {
|
||||
const { data, error } = useSWR<PublicSettingsResponse>(
|
||||
'/api/v1/settings/public',
|
||||
{ initialData: currentSettings }
|
||||
);
|
||||
|
||||
let newSettings = defaultSettings;
|
||||
|
||||
if (data && !error) {
|
||||
newSettings = data;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={{ currentSettings: newSettings }}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -29,6 +29,20 @@
|
||||
"components.Login.signinplex": "Sign in to continue",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
||||
"components.MovieDetails.MovieCrew.fullcrew": "Full Crew",
|
||||
"components.MovieDetails.RequestButton.approve4krequests": "Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.MovieDetails.RequestButton.approverequest": "Approve Request",
|
||||
"components.MovieDetails.RequestButton.approverequest4k": "Approve 4K Request",
|
||||
"components.MovieDetails.RequestButton.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.MovieDetails.RequestButton.decline4krequests": "Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.MovieDetails.RequestButton.declinerequest": "Decline Request",
|
||||
"components.MovieDetails.RequestButton.declinerequest4k": "Decline 4K Request",
|
||||
"components.MovieDetails.RequestButton.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.MovieDetails.RequestButton.request": "Request",
|
||||
"components.MovieDetails.RequestButton.request4k": "Request 4K",
|
||||
"components.MovieDetails.RequestButton.requestmore": "Request More",
|
||||
"components.MovieDetails.RequestButton.requestmore4k": "Request More 4K",
|
||||
"components.MovieDetails.RequestButton.viewrequest": "View Request",
|
||||
"components.MovieDetails.RequestButton.viewrequest4k": "View 4K Request",
|
||||
"components.MovieDetails.approve": "Approve",
|
||||
"components.MovieDetails.available": "Available",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
@@ -47,7 +61,6 @@
|
||||
"components.MovieDetails.recommendations": "Recommendations",
|
||||
"components.MovieDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.MovieDetails.releasedate": "Release Date",
|
||||
"components.MovieDetails.request": "Request",
|
||||
"components.MovieDetails.revenue": "Revenue",
|
||||
"components.MovieDetails.runtime": "{minutes} minutes",
|
||||
"components.MovieDetails.similar": "Similar Titles",
|
||||
@@ -58,7 +71,6 @@
|
||||
"components.MovieDetails.userrating": "User Rating",
|
||||
"components.MovieDetails.view": "View",
|
||||
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
||||
"components.MovieDetails.viewrequest": "View Request",
|
||||
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
||||
"components.NotificationTypeSelector.mediaapproved": "Media Approved",
|
||||
"components.NotificationTypeSelector.mediaapprovedDescription": "Sends a notification when media is approved.",
|
||||
@@ -105,8 +117,12 @@
|
||||
"components.RequestModal.extras": "Extras",
|
||||
"components.RequestModal.notrequested": "Not Requested",
|
||||
"components.RequestModal.numberofepisodes": "# of Episodes",
|
||||
"components.RequestModal.pending4krequest": "Pending request for {title} in 4K",
|
||||
"components.RequestModal.pendingrequest": "Pending request for {title}",
|
||||
"components.RequestModal.request": "Request",
|
||||
"components.RequestModal.request4k": "Request 4K",
|
||||
"components.RequestModal.request4kfrom": "There is currently a pending 4K request from {username}",
|
||||
"components.RequestModal.request4ktitle": "Request {title} in 4K",
|
||||
"components.RequestModal.requestCancel": "Request for <strong>{title}</strong> cancelled",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> requested.",
|
||||
"components.RequestModal.requestadmin": "Your request will be immediately approved.",
|
||||
@@ -303,7 +319,6 @@
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Services",
|
||||
"components.Settings.nextexecution": "Next Execution",
|
||||
"components.Settings.no4kimplemented": "(Default 4K servers are not currently implemented)",
|
||||
"components.Settings.nodefault": "No default server selected!",
|
||||
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
||||
"components.Settings.notificationsettings": "Notification Settings",
|
||||
@@ -344,6 +359,7 @@
|
||||
"components.Setup.tip": "Tip",
|
||||
"components.Setup.welcome": "Welcome to Overseerr",
|
||||
"components.Slider.noresults": "No Results",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.StatusChacker.newversionDescription": "An update is now available. Click the button below to reload the application.",
|
||||
"components.StatusChacker.newversionavailable": "New Version Available",
|
||||
"components.StatusChacker.reloadOverseerr": "Reload Overseerr",
|
||||
@@ -353,12 +369,10 @@
|
||||
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.approve": "Approve",
|
||||
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.available": "Available",
|
||||
"components.TvDetails.cancelrequest": "Cancel Request",
|
||||
"components.TvDetails.cast": "Cast",
|
||||
"components.TvDetails.decline": "Decline",
|
||||
"components.TvDetails.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.firstAirDate": "First Air Date",
|
||||
"components.TvDetails.manageModalClearMedia": "Clear All Media Data",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "This will remove all media data including all requests for this item, irreversibly. If this item exists in your Plex library, the media info will be recreated next sync.",
|
||||
@@ -372,8 +386,6 @@
|
||||
"components.TvDetails.pending": "Pending",
|
||||
"components.TvDetails.recommendations": "Recommendations",
|
||||
"components.TvDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.TvDetails.request": "Request",
|
||||
"components.TvDetails.requestmore": "Request More",
|
||||
"components.TvDetails.showtype": "Show Type",
|
||||
"components.TvDetails.similar": "Similar Series",
|
||||
"components.TvDetails.similarsubtext": "Other series similar to {title}",
|
||||
@@ -397,6 +409,12 @@
|
||||
"components.UserEdit.managerequestsDescription": "Grants permission to manage Overseerr requests. This includes approving and denying requests.",
|
||||
"components.UserEdit.permissions": "Permissions",
|
||||
"components.UserEdit.request": "Request",
|
||||
"components.UserEdit.request4k": "Request 4K",
|
||||
"components.UserEdit.request4kDescription": "Grants permission to request 4K movies and series.",
|
||||
"components.UserEdit.request4kMovies": "Request 4K Movies",
|
||||
"components.UserEdit.request4kMoviesDescription": "Grants permission to request 4K movies.",
|
||||
"components.UserEdit.request4kTv": "Request 4K Series",
|
||||
"components.UserEdit.request4kTvDescription": "Grants permission to request 4K Series.",
|
||||
"components.UserEdit.requestDescription": "Grants permission to request movies and series.",
|
||||
"components.UserEdit.save": "Save",
|
||||
"components.UserEdit.saving": "Saving…",
|
||||
|
||||
@@ -14,6 +14,8 @@ import Head from 'next/head';
|
||||
import Toast from '../components/Toast';
|
||||
import { InteractionProvider } from '../context/InteractionContext';
|
||||
import StatusChecker from '../components/StatusChacker';
|
||||
import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
|
||||
import { SettingsProvider } from '../context/SettingsContext';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const loadLocaleData = (locale: string): Promise<any> => {
|
||||
@@ -55,6 +57,7 @@ interface ExtendedAppProps extends AppProps {
|
||||
user: User;
|
||||
messages: MessagesType;
|
||||
locale: AvailableLocales;
|
||||
currentSettings: PublicSettingsResponse;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -68,6 +71,7 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
user,
|
||||
messages,
|
||||
locale,
|
||||
currentSettings,
|
||||
}: ExtendedAppProps) => {
|
||||
let component: React.ReactNode;
|
||||
const [loadedMessages, setMessages] = useState<MessagesType>(messages);
|
||||
@@ -103,15 +107,17 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
defaultLocale="en"
|
||||
messages={loadedMessages}
|
||||
>
|
||||
<InteractionProvider>
|
||||
<ToastProvider components={{ Toast }}>
|
||||
<Head>
|
||||
<title>Overseerr</title>
|
||||
</Head>
|
||||
<StatusChecker />
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</InteractionProvider>
|
||||
<SettingsProvider currentSettings={currentSettings}>
|
||||
<InteractionProvider>
|
||||
<ToastProvider components={{ Toast }}>
|
||||
<Head>
|
||||
<title>Overseerr</title>
|
||||
</Head>
|
||||
<StatusChecker />
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</InteractionProvider>
|
||||
</SettingsProvider>
|
||||
</IntlProvider>
|
||||
</LanguageContext.Provider>
|
||||
</SWRConfig>
|
||||
@@ -121,15 +127,22 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
CoreApp.getInitialProps = async (initialProps) => {
|
||||
const { ctx, router } = initialProps;
|
||||
let user = undefined;
|
||||
let currentSettings: PublicSettingsResponse = {
|
||||
initialized: false,
|
||||
movie4kEnabled: false,
|
||||
series4kEnabled: false,
|
||||
};
|
||||
|
||||
let locale = 'en';
|
||||
|
||||
if (ctx.res) {
|
||||
// Check if app is initialized and redirect if necessary
|
||||
const response = await axios.get<{ initialized: boolean }>(
|
||||
const response = await axios.get<PublicSettingsResponse>(
|
||||
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
|
||||
);
|
||||
|
||||
currentSettings = response.data;
|
||||
|
||||
const initialized = response.data.initialized;
|
||||
|
||||
if (!initialized) {
|
||||
@@ -181,7 +194,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
||||
|
||||
const messages = await loadLocaleData(locale);
|
||||
|
||||
return { ...appInitialProps, user, messages, locale };
|
||||
return { ...appInitialProps, user, messages, locale, currentSettings };
|
||||
};
|
||||
|
||||
export default CoreApp;
|
||||
|
||||
Reference in New Issue
Block a user