feat(ui): Add support for requesting collections in 4K (#968)

pull/973/head
TheCatLady 3 years ago committed by GitHub
parent 2e35aff903
commit 139341b043
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,16 +8,17 @@ import { MediaStatus } from '../../../server/constants/media';
import type { MediaRequest } from '../../../server/entity/MediaRequest'; import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { Collection } from '../../../server/models/Collection'; import type { Collection } from '../../../server/models/Collection';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Badge from '../Common/Badge'; import StatusBadge from '../StatusBadge';
import Button from '../Common/Button'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
import Modal from '../Common/Modal'; import Modal from '../Common/Modal';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard'; import TitleCard from '../TitleCard';
import Transition from '../Transition'; import Transition from '../Transition';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import { useUser, Permission } from '../../hooks/useUser';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({ const messages = defineMessages({
overviewunavailable: 'Overview unavailable.', overviewunavailable: 'Overview unavailable.',
@ -29,6 +30,10 @@ const messages = defineMessages({
requestcollection: 'Request Collection', requestcollection: 'Request Collection',
requestswillbecreated: requestswillbecreated:
'The following titles will have requests created for them:', 'The following titles will have requests created for them:',
request4k: 'Request 4K',
requestcollection4k: 'Request Collection in 4K',
requestswillbecreated4k:
'The following titles will have 4K requests created for them:',
requestSuccess: '<strong>{title}</strong> successfully requested!', requestSuccess: '<strong>{title}</strong> successfully requested!',
}); });
@ -41,10 +46,14 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const router = useRouter(); const router = useRouter();
const settings = useSettings();
const { addToast } = useToasts(); const { addToast } = useToasts();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { hasPermission } = useUser();
const [requestModal, setRequestModal] = useState(false); const [requestModal, setRequestModal] = useState(false);
const [isRequesting, setRequesting] = useState(false); const [isRequesting, setRequesting] = useState(false);
const [is4k, setIs4k] = useState(false);
const { data, error, revalidate } = useSWR<Collection>( const { data, error, revalidate } = useSWR<Collection>(
`/api/v1/collection/${router.query.collectionId}?language=${locale}`, `/api/v1/collection/${router.query.collectionId}?language=${locale}`,
{ {
@ -61,8 +70,45 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
return <Error statusCode={404} />; return <Error statusCode={404} />;
} }
let collectionStatus = MediaStatus.UNKNOWN;
let collectionStatus4k = MediaStatus.UNKNOWN;
if (
data.parts.every(
(part) =>
part.mediaInfo && part.mediaInfo.status === MediaStatus.AVAILABLE
)
) {
collectionStatus = MediaStatus.AVAILABLE;
} else if (
data.parts.some(
(part) =>
part.mediaInfo && part.mediaInfo.status === MediaStatus.AVAILABLE
)
) {
collectionStatus = MediaStatus.PARTIALLY_AVAILABLE;
}
if (
data.parts.every(
(part) =>
part.mediaInfo && part.mediaInfo.status4k === MediaStatus.AVAILABLE
)
) {
collectionStatus4k = MediaStatus.AVAILABLE;
} else if (
data.parts.some(
(part) =>
part.mediaInfo && part.mediaInfo.status4k === MediaStatus.AVAILABLE
)
) {
collectionStatus4k = MediaStatus.PARTIALLY_AVAILABLE;
}
const requestableParts = data.parts.filter( const requestableParts = data.parts.filter(
(part) => !part.mediaInfo || part.mediaInfo.status === MediaStatus.UNKNOWN (part) =>
!part.mediaInfo ||
part.mediaInfo[is4k ? 'status4k' : 'status'] === MediaStatus.UNKNOWN
); );
const requestBundle = async () => { const requestBundle = async () => {
@ -73,6 +119,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
await axios.post<MediaRequest>('/api/v1/request', { await axios.post<MediaRequest>('/api/v1/request', {
mediaId: part.id, mediaId: part.id,
mediaType: 'movie', mediaType: 'movie',
is4k,
}); });
}) })
); );
@ -123,12 +170,14 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
okText={ okText={
isRequesting isRequesting
? intl.formatMessage(messages.requesting) ? intl.formatMessage(messages.requesting)
: intl.formatMessage(messages.request) : intl.formatMessage(is4k ? messages.request4k : messages.request)
} }
okDisabled={isRequesting} okDisabled={isRequesting}
okButtonType="primary" okButtonType="primary"
onCancel={() => setRequestModal(false)} onCancel={() => setRequestModal(false)}
title={intl.formatMessage(messages.requestcollection)} title={intl.formatMessage(
is4k ? messages.requestcollection4k : messages.requestcollection
)}
iconSvg={ iconSvg={
<svg <svg
className="w-6 h-6" className="w-6 h-6"
@ -146,13 +195,20 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
</svg> </svg>
} }
> >
<p>{intl.formatMessage(messages.requestswillbecreated)}</p> <p>
{intl.formatMessage(
is4k
? messages.requestswillbecreated4k
: messages.requestswillbecreated
)}
</p>
<ul className="py-4 pl-8 list-disc"> <ul className="py-4 pl-8 list-disc">
{data.parts {data.parts
.filter( .filter(
(part) => (part) =>
!part.mediaInfo || !part.mediaInfo ||
part.mediaInfo?.status === MediaStatus.UNKNOWN part.mediaInfo[is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN
) )
.map((part) => ( .map((part) => (
<li key={`request-part-${part.id}`}>{part.title}</li> <li key={`request-part-${part.id}`}>{part.title}</li>
@ -160,64 +216,128 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
</ul> </ul>
</Modal> </Modal>
</Transition> </Transition>
<div className="flex flex-col items-center pt-4 md:flex-row md:items-end"> <div className="flex flex-col items-center pt-4 lg:flex-row lg:items-end">
<div className="flex-shrink-0 md:mr-4"> <div className="lg:mr-4">
<img <img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`} src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
alt="" alt=""
className="w-32 rounded shadow md:rounded-lg md:shadow-2xl md:w-52" className="w-32 rounded shadow md:rounded-lg md:shadow-2xl md:w-44 lg:w-52"
/> />
</div> </div>
<div className="flex flex-col mt-4 text-center text-white md:mr-4 md:mt-0 md:text-left"> <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"> <div className="mb-2 space-x-2">
{data.parts.every( <span className="ml-2 lg:ml-0">
(part) => part.mediaInfo?.status === MediaStatus.AVAILABLE <StatusBadge
) && ( status={collectionStatus}
<Badge badgeType="success"> inProgress={data.parts.some(
{intl.formatMessage(globalMessages.available)} (part) => (part.mediaInfo?.downloadStatus ?? []).length > 0
</Badge> )}
)} />
{!data.parts.every( </span>
(part) => part.mediaInfo?.status === MediaStatus.AVAILABLE {settings.currentSettings.movie4kEnabled &&
) && hasPermission(
data.parts.some( [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
(part) => part.mediaInfo?.status === MediaStatus.AVAILABLE {
type: 'or',
}
) && ( ) && (
<Badge badgeType="success"> <span>
{intl.formatMessage(globalMessages.partiallyavailable)} <StatusBadge
</Badge> status={collectionStatus4k}
is4k
inProgress={data.parts.some(
(part) =>
(part.mediaInfo?.downloadStatus4k ?? []).length > 0
)}
/>
</span>
)} )}
</div> </div>
<h1 className="text-2xl md:text-4xl">{data.name}</h1> <h1 className="text-2xl md:text-4xl">{data.name}</h1>
<span className="mt-1 text-xs md:text-base md:mt-0"> <span className="mt-1 text-xs lg:text-base lg:mt-0">
{intl.formatMessage(messages.numberofmovies, { {intl.formatMessage(messages.numberofmovies, {
count: data.parts.length, count: data.parts.length,
})} })}
</span> </span>
</div> </div>
<div className="flex justify-end flex-1 mt-4 md:mt-0"> <div className="relative z-10 flex flex-wrap justify-center flex-shrink-0 mt-4 sm:justify-end sm:flex-nowrap lg:mt-0">
{data.parts.some( {hasPermission(Permission.REQUEST) &&
(part) => (collectionStatus !== MediaStatus.AVAILABLE ||
!part.mediaInfo || part.mediaInfo?.status === MediaStatus.UNKNOWN (settings.currentSettings.movie4kEnabled &&
) && ( hasPermission(
<Button buttonType="primary" onClick={() => setRequestModal(true)}> [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
<svg { type: 'or' }
className="w-4 mr-1" ) &&
fill="none" collectionStatus4k !== MediaStatus.AVAILABLE)) && (
stroke="currentColor" <div className="mb-3 sm:mb-0">
viewBox="0 0 24 24" <ButtonWithDropdown
xmlns="http://www.w3.org/2000/svg" buttonType="primary"
> onClick={() => {
<path setRequestModal(true);
strokeLinecap="round" setIs4k(collectionStatus === MediaStatus.AVAILABLE);
strokeLinejoin="round" }}
strokeWidth={2} text={
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" <>
/> <svg
</svg> className="w-4 mr-1"
{intl.formatMessage(messages.requestcollection)} fill="none"
</Button> 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>
<span>
{intl.formatMessage(
collectionStatus === MediaStatus.AVAILABLE
? messages.requestcollection4k
: messages.requestcollection
)}
</span>
</>
}
>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
collectionStatus !== MediaStatus.AVAILABLE &&
collectionStatus4k !== MediaStatus.AVAILABLE && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<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>
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
</div>
)}
</div> </div>
</div> </div>
<div className="flex flex-col pt-8 pb-4 text-white md:flex-row"> <div className="flex flex-col pt-8 pb-4 text-white md:flex-row">

@ -378,31 +378,31 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div> </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="flex flex-col flex-1 mt-4 text-center text-white lg:mr-4 lg:mt-0 lg:text-left">
<div className="mb-2 space-x-2"> <div className="mb-2 space-x-2">
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && ( <span className="ml-2 lg:ml-0">
<span className="ml-2 lg:ml-0">
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl}
plexUrl4k={data.mediaInfo?.plexUrl4k}
/>
</span>
)}
<span>
<StatusBadge <StatusBadge
status={data.mediaInfo?.status4k} status={data.mediaInfo?.status}
is4k inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
inProgress={(data.mediaInfo?.downloadStatus4k ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
plexUrl4k={
data.mediaInfo?.plexUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
? data.mediaInfo.plexUrl4k
: undefined
}
/> />
</span> </span>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{
type: 'or',
}
) && (
<span>
<StatusBadge
status={data.mediaInfo?.status4k}
is4k
inProgress={
(data.mediaInfo?.downloadStatus4k ?? []).length > 0
}
plexUrl4k={data.mediaInfo?.plexUrl4k}
/>
</span>
)}
</div> </div>
<h1 className="text-2xl lg:text-4xl"> <h1 className="text-2xl lg:text-4xl">
{data.title}{' '} {data.title}{' '}

@ -190,7 +190,8 @@ const RequestItem: React.FC<RequestItemProps> = ({
</div> </div>
</Table.TD> </Table.TD>
<Table.TD> <Table.TD>
{requestData.media.status === MediaStatus.UNKNOWN || {requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN ||
requestData.status === MediaRequestStatus.DECLINED ? ( requestData.status === MediaRequestStatus.DECLINED ? (
<Badge badgeType="danger"> <Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED {requestData.status === MediaRequestStatus.DECLINED
@ -247,7 +248,8 @@ const RequestItem: React.FC<RequestItemProps> = ({
</div> </div>
</Table.TD> </Table.TD>
<Table.TD alignText="right"> <Table.TD alignText="right">
{requestData.media.status === MediaStatus.UNKNOWN && {requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED && requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<Button <Button

@ -10,6 +10,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { useIsTouch } from '../../hooks/useIsTouch'; import { useIsTouch } from '../../hooks/useIsTouch';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Spinner from '../../assets/spinner.svg'; import Spinner from '../../assets/spinner.svg';
import { useUser, Permission } from '../../hooks/useUser';
const messages = defineMessages({ const messages = defineMessages({
movie: 'Movie', movie: 'Movie',
@ -42,6 +43,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
}) => { }) => {
const isTouch = useIsTouch(); const isTouch = useIsTouch();
const intl = useIntl(); const intl = useIntl();
const { hasPermission } = useUser();
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [currentStatus, setCurrentStatus] = useState(status); const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
@ -204,7 +206,8 @@ const TitleCard: React.FC<TitleCardProps> = ({
<div className="flex items-end w-full h-full"> <div className="flex items-end w-full h-full">
<div <div
className={`px-2 text-white ${ className={`px-2 text-white ${
currentStatus && currentStatus !== MediaStatus.UNKNOWN !hasPermission(Permission.REQUEST) ||
(currentStatus && currentStatus !== MediaStatus.UNKNOWN)
? 'pb-2' ? 'pb-2'
: 'pb-11' : 'pb-11'
}`} }`}
@ -218,8 +221,9 @@ const TitleCard: React.FC<TitleCardProps> = ({
className="text-xs whitespace-normal" className="text-xs whitespace-normal"
style={{ style={{
WebkitLineClamp: WebkitLineClamp:
currentStatus && !hasPermission(Permission.REQUEST) ||
currentStatus !== MediaStatus.UNKNOWN (currentStatus &&
currentStatus !== MediaStatus.UNKNOWN)
? 5 ? 5
: 3, : 3,
display: '-webkit-box', display: '-webkit-box',
@ -235,33 +239,34 @@ const TitleCard: React.FC<TitleCardProps> = ({
</Link> </Link>
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2"> <div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
{(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && ( {hasPermission(Permission.REQUEST) &&
<button (!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
onClick={(e) => { <button
e.preventDefault(); onClick={(e) => {
setShowRequestModal(true); e.preventDefault();
}} setShowRequestModal(true);
className="flex items-center justify-center w-full text-white transition duration-150 ease-in-out bg-indigo-600 rounded-md h-7 hover:bg-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700" }}
> className="flex items-center justify-center w-full text-white transition duration-150 ease-in-out bg-indigo-600 rounded-md h-7 hover:bg-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700"
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<path <svg
strokeLinecap="round" className="w-4 mr-1"
strokeLinejoin="round" fill="none"
strokeWidth={2} stroke="currentColor"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" viewBox="0 0 24 24"
/> xmlns="http://www.w3.org/2000/svg"
</svg> >
<span className="text-xs"> <path
{intl.formatMessage(globalMessages.request)} strokeLinecap="round"
</span> strokeLinejoin="round"
</button> strokeWidth={2}
)} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span className="text-xs">
{intl.formatMessage(globalMessages.request)}
</span>
</button>
)}
</div> </div>
</div> </div>
</Transition> </Transition>

@ -400,31 +400,28 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</div> </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="flex flex-col flex-1 mt-4 text-center text-white lg:mr-4 lg:mt-0 lg:text-left">
<div className="mb-2 space-x-2"> <div className="mb-2 space-x-2">
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && ( <span className="ml-2 lg:ml-0">
<span className="ml-2 lg:ml-0">
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl}
plexUrl4k={data.mediaInfo?.plexUrl4k}
/>
</span>
)}
<span>
<StatusBadge <StatusBadge
status={data.mediaInfo?.status4k} status={data.mediaInfo?.status}
is4k
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
plexUrl4k={
data.mediaInfo?.plexUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_TV))
? data.mediaInfo.plexUrl4k
: undefined
}
/> />
</span> </span>
{settings.currentSettings.series4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
type: 'or',
}) && (
<span>
<StatusBadge
status={data.mediaInfo?.status4k}
is4k
inProgress={
(data.mediaInfo?.downloadStatus ?? []).length > 0
}
plexUrl4k={data.mediaInfo?.plexUrl4k}
/>
</span>
)}
</div> </div>
<h1 className="text-2xl lg:text-4xl"> <h1 className="text-2xl lg:text-4xl">
{data.name}{' '} {data.name}{' '}

@ -6,10 +6,13 @@
"components.CollectionDetails.overview": "Overview", "components.CollectionDetails.overview": "Overview",
"components.CollectionDetails.overviewunavailable": "Overview unavailable.", "components.CollectionDetails.overviewunavailable": "Overview unavailable.",
"components.CollectionDetails.request": "Request", "components.CollectionDetails.request": "Request",
"components.CollectionDetails.request4k": "Request 4K",
"components.CollectionDetails.requestSuccess": "<strong>{title}</strong> successfully requested!", "components.CollectionDetails.requestSuccess": "<strong>{title}</strong> successfully requested!",
"components.CollectionDetails.requestcollection": "Request Collection", "components.CollectionDetails.requestcollection": "Request Collection",
"components.CollectionDetails.requestcollection4k": "Request Collection in 4K",
"components.CollectionDetails.requesting": "Requesting…", "components.CollectionDetails.requesting": "Requesting…",
"components.CollectionDetails.requestswillbecreated": "The following titles will have requests created for them:", "components.CollectionDetails.requestswillbecreated": "The following titles will have requests created for them:",
"components.CollectionDetails.requestswillbecreated4k": "The following titles will have 4K requests created for them:",
"components.Common.ListView.noresults": "No results.", "components.Common.ListView.noresults": "No results.",
"components.Discover.discover": "Discover", "components.Discover.discover": "Discover",
"components.Discover.discovermovies": "Popular Movies", "components.Discover.discovermovies": "Popular Movies",

Loading…
Cancel
Save