mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-31 19:59:31 -05:00
feat(media): add link to the item on plex (#735)
Co-authored-by: sct <sctsnipe@gmail.com>
This commit is contained in:
1
src/assets/services/plex.svg
Normal file
1
src/assets/services/plex.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" data-name="Layer 1" viewBox="0 0 320.03 103.61" style=" "><defs><style>.cls-1{fill:#fff;}.cls-2{fill:url(#radial-gradient);}.cls-3{fill:#e5a00d;}</style><radialGradient id="radial-gradient" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset="0.17" stop-color="#f9be03"/><stop offset="0.51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><title>plex-logo</title><polygon id="X" class="cls-1" points="320.03 -0.09 289.96 -0.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79 320.03 -0.09"/><g id="chevron"><polygon class="cls-2" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -0.09"/><polygon class="cls-3" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -0.09"/></g><path id="E" class="cls-1" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z"/><path id="L" class="cls-1" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z"/><path id="P" class="cls-1" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -22,7 +22,7 @@ const Badge: React.FC<BadgeProps> = ({
|
||||
badgeStyle.push('bg-yellow-500 text-yellow-100');
|
||||
break;
|
||||
case 'success':
|
||||
badgeStyle.push('bg-green-400 text-green-100');
|
||||
badgeStyle.push('bg-green-500 text-green-100');
|
||||
break;
|
||||
default:
|
||||
badgeStyle.push('bg-indigo-500 text-indigo-100');
|
||||
|
||||
@@ -9,22 +9,41 @@ import useClickOutside from '../../../hooks/useClickOutside';
|
||||
import Transition from '../../Transition';
|
||||
import { withProperties } from '../../../utils/typeHelpers';
|
||||
|
||||
const DropdownItem: React.FC<AnchorHTMLAttributes<HTMLAnchorElement>> = ({
|
||||
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
buttonType?: 'primary' | 'ghost';
|
||||
}
|
||||
|
||||
const DropdownItem: React.FC<DropdownItemProps> = ({
|
||||
children,
|
||||
buttonType = 'primary',
|
||||
...props
|
||||
}) => (
|
||||
<a
|
||||
className="flex items-center px-4 py-2 text-sm leading-5 text-white bg-indigo-600 cursor-pointer hover:bg-indigo-500 hover:text-white focus:outline-none focus:border-indigo-700 focus:text-white"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}) => {
|
||||
let styleClass = '';
|
||||
|
||||
switch (buttonType) {
|
||||
case 'ghost':
|
||||
styleClass =
|
||||
'text-white bg-gray-700 hover:bg-gray-600 hover:text-white focus:border-gray-500 focus:text-white';
|
||||
break;
|
||||
default:
|
||||
styleClass =
|
||||
'text-white bg-indigo-600 hover:bg-indigo-500 hover:text-white focus:border-indigo-700 focus:text-white';
|
||||
}
|
||||
return (
|
||||
<a
|
||||
className={`flex items-center px-4 py-2 text-sm leading-5 cursor-pointer focus:outline-none ${styleClass}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonWithDropdownProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
text: ReactNode;
|
||||
dropdownIcon?: ReactNode;
|
||||
buttonType?: 'primary' | 'ghost';
|
||||
}
|
||||
|
||||
const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
|
||||
@@ -32,29 +51,52 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
|
||||
children,
|
||||
dropdownIcon,
|
||||
className,
|
||||
buttonType = 'primary',
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useClickOutside(buttonRef, () => setIsOpen(false));
|
||||
|
||||
const styleClasses = {
|
||||
mainButtonClasses: '',
|
||||
dropdownSideButtonClasses: '',
|
||||
dropdownClasses: '',
|
||||
};
|
||||
|
||||
switch (buttonType) {
|
||||
case 'ghost':
|
||||
styleClasses.mainButtonClasses =
|
||||
'text-white bg-transparent border border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
|
||||
styleClasses.dropdownSideButtonClasses =
|
||||
'bg-transparent border border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
|
||||
styleClasses.dropdownClasses = 'bg-gray-700';
|
||||
break;
|
||||
default:
|
||||
styleClasses.mainButtonClasses =
|
||||
'text-white bg-indigo-600 hover:text-white hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
|
||||
styleClasses.dropdownSideButtonClasses =
|
||||
'bg-indigo-700 border border-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
|
||||
styleClasses.dropdownClasses = 'bg-indigo-600';
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="relative z-0 inline-flex h-full rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-full items-center px-4 py-2 text-white bg-indigo-600 hover:bg-indigo-500 text-sm leading-5 font-medium hover:text-white focus:ring-indigo active:bg-indigo-700 focus:z-10 focus:outline-none focus:ring-blue transition ease-in-out duration-150 ${
|
||||
children ? 'rounded-l-md' : 'rounded-md'
|
||||
} ${className}`}
|
||||
className={`relative inline-flex h-full items-center px-4 py-2 text-sm leading-5 font-medium z-10 hover:z-20 focus:z-20 focus:outline-none transition ease-in-out duration-150 ${
|
||||
styleClasses.mainButtonClasses
|
||||
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
|
||||
ref={buttonRef}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
<span className="relative block -ml-px">
|
||||
<span className="relative z-10 block -ml-px">
|
||||
{children && (
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center h-full px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-700 border border-indigo-600 rounded-r-md hover:bg-indigo-500 focus:z-10 focus:outline-none active:bg-indigo-700 focus:ring-blue"
|
||||
className={`relative inline-flex items-center h-full px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out rounded-r-md focus:z-10 ${styleClasses.dropdownSideButtonClasses}`}
|
||||
aria-label="Expand"
|
||||
onClick={() => setIsOpen((state) => !state)}
|
||||
>
|
||||
@@ -86,7 +128,9 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div className="absolute right-0 w-56 mt-2 -mr-1 origin-top-right rounded-md shadow-lg">
|
||||
<div className="bg-indigo-600 rounded-md ring-1 ring-black ring-opacity-5">
|
||||
<div
|
||||
className={`rounded-md ring-1 ring-black ring-opacity-5 ${styleClasses.dropdownClasses}`}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,14 @@ import React from 'react';
|
||||
import TmdbLogo from '../../assets/services/tmdb.svg';
|
||||
import ImdbLogo from '../../assets/services/imdb.svg';
|
||||
import RTLogo from '../../assets/services/rt.svg';
|
||||
import PlexLogo from '../../assets/services/plex.svg';
|
||||
|
||||
interface ExternalLinkBlockProps {
|
||||
mediaType: 'movie' | 'tv';
|
||||
imdbId?: string;
|
||||
tmdbId?: number;
|
||||
rtUrl?: string;
|
||||
plexUrl?: string;
|
||||
}
|
||||
|
||||
const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
|
||||
@@ -15,9 +17,20 @@ const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
|
||||
tmdbId,
|
||||
rtUrl,
|
||||
mediaType,
|
||||
plexUrl,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-end items-center">
|
||||
{plexUrl && (
|
||||
<a
|
||||
href={plexUrl}
|
||||
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<PlexLogo />
|
||||
</a>
|
||||
)}
|
||||
{tmdbId && (
|
||||
<a
|
||||
href={`https://www.themoviedb.org/${mediaType}/${tmdbId}`}
|
||||
|
||||
@@ -35,6 +35,7 @@ import RequestButton from '../RequestButton';
|
||||
import MediaSlider from '../MediaSlider';
|
||||
import ConfirmButton from '../Common/ConfirmButton';
|
||||
import DownloadBlock from '../DownloadBlock';
|
||||
import ButtonWithDropdown from '../Common/ButtonWithDropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
releasedate: 'Release Date',
|
||||
@@ -69,6 +70,8 @@ const messages = defineMessages({
|
||||
openradarr: 'Open Movie in Radarr',
|
||||
openradarr4k: 'Open Movie in 4K Radarr',
|
||||
downloadstatus: 'Download Status',
|
||||
playonplex: 'Play on Plex',
|
||||
play4konplex: 'Play 4K on Plex',
|
||||
});
|
||||
|
||||
interface MovieDetailsProps {
|
||||
@@ -252,6 +255,8 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
<StatusBadge
|
||||
status={data.mediaInfo?.status}
|
||||
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
|
||||
plexUrl={data.mediaInfo?.plexUrl}
|
||||
plexUrl4k={data.mediaInfo?.plexUrl4k}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
@@ -260,6 +265,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
status={data.mediaInfo?.status4k}
|
||||
is4k
|
||||
inProgress={(data.mediaInfo?.downloadStatus4k ?? []).length > 0}
|
||||
plexUrl={data.mediaInfo?.plexUrl}
|
||||
plexUrl4k={
|
||||
data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_MOVIE))
|
||||
? data.mediaInfo.plexUrl4k
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -281,37 +294,86 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-wrap justify-center flex-shrink-0 mt-4 sm:justify-end sm:flex-nowrap lg:mt-0">
|
||||
{trailerUrl && (
|
||||
<a
|
||||
href={trailerUrl}
|
||||
target={'_blank'}
|
||||
rel="noreferrer"
|
||||
className="mb-3 sm:mb-0"
|
||||
{(trailerUrl ||
|
||||
data.mediaInfo?.plexUrl ||
|
||||
data.mediaInfo?.plexUrl4k) && (
|
||||
<ButtonWithDropdown
|
||||
buttonType="ghost"
|
||||
text={
|
||||
<>
|
||||
<svg
|
||||
className="w-5 h-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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{data.mediaInfo?.plexUrl
|
||||
? intl.formatMessage(messages.playonplex)
|
||||
: data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_MOVIE))
|
||||
? intl.formatMessage(messages.playonplex)
|
||||
: intl.formatMessage(messages.watchtrailer)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
if (data.mediaInfo?.plexUrl) {
|
||||
window.open(data.mediaInfo?.plexUrl, '_blank');
|
||||
} else if (data.mediaInfo?.plexUrl4k) {
|
||||
window.open(data.mediaInfo?.plexUrl4k, '_blank');
|
||||
} else if (trailerUrl) {
|
||||
window.open(trailerUrl, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button buttonType="ghost">
|
||||
<svg
|
||||
className="w-5 h-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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.watchtrailer} />
|
||||
</Button>
|
||||
</a>
|
||||
{data.mediaInfo?.plexUrl ||
|
||||
(data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_MOVIE))) ? (
|
||||
<>
|
||||
{data.mediaInfo?.plexUrl &&
|
||||
data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_MOVIE)) && (
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => {
|
||||
window.open(data.mediaInfo?.plexUrl4k, '_blank');
|
||||
}}
|
||||
buttonType="ghost"
|
||||
>
|
||||
{intl.formatMessage(messages.play4konplex)}
|
||||
</ButtonWithDropdown.Item>
|
||||
)}
|
||||
{(data.mediaInfo?.plexUrl || data.mediaInfo?.plexUrl4k) &&
|
||||
trailerUrl && (
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => {
|
||||
window.open(trailerUrl, '_blank');
|
||||
}}
|
||||
buttonType="ghost"
|
||||
>
|
||||
{intl.formatMessage(messages.watchtrailer)}
|
||||
</ButtonWithDropdown.Item>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</ButtonWithDropdown>
|
||||
)}
|
||||
<div className="mb-3 sm:mb-0">
|
||||
<RequestButton
|
||||
@@ -550,6 +612,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
tmdbId={data.id}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.plexUrl4k}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,18 +13,37 @@ interface StatusBadgeProps {
|
||||
status?: MediaStatus;
|
||||
is4k?: boolean;
|
||||
inProgress?: boolean;
|
||||
plexUrl?: string;
|
||||
plexUrl4k?: string;
|
||||
}
|
||||
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
status,
|
||||
is4k = false,
|
||||
inProgress = false,
|
||||
plexUrl,
|
||||
plexUrl4k,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (is4k) {
|
||||
switch (status) {
|
||||
case MediaStatus.AVAILABLE:
|
||||
if (plexUrl4k) {
|
||||
return (
|
||||
<a href={plexUrl4k} target="_blank" rel="noopener noreferrer">
|
||||
<Badge
|
||||
badgeType="success"
|
||||
className="transition cursor-pointer hover:bg-green-400"
|
||||
>
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.available),
|
||||
})}
|
||||
</Badge>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
@@ -33,6 +52,21 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
</Badge>
|
||||
);
|
||||
case MediaStatus.PARTIALLY_AVAILABLE:
|
||||
if (plexUrl4k) {
|
||||
return (
|
||||
<a href={plexUrl4k} target="_blank" rel="noopener noreferrer">
|
||||
<Badge
|
||||
badgeType="success"
|
||||
className="transition cursor-pointer hover:bg-green-400"
|
||||
>
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(globalMessages.partiallyavailable),
|
||||
})}
|
||||
</Badge>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
@@ -70,6 +104,22 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
|
||||
switch (status) {
|
||||
case MediaStatus.AVAILABLE:
|
||||
if (plexUrl) {
|
||||
return (
|
||||
<a href={plexUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Badge
|
||||
badgeType="success"
|
||||
className="transition cursor-pointer hover:bg-green-400"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{intl.formatMessage(globalMessages.available)}</span>
|
||||
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
|
||||
</div>
|
||||
</Badge>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
<div className="flex items-center">
|
||||
@@ -79,6 +129,24 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
</Badge>
|
||||
);
|
||||
case MediaStatus.PARTIALLY_AVAILABLE:
|
||||
if (plexUrl) {
|
||||
return (
|
||||
<a href={plexUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Badge
|
||||
badgeType="success"
|
||||
className="transition cursor-pointer hover:bg-green-400"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{intl.formatMessage(globalMessages.partiallyavailable)}
|
||||
</span>
|
||||
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
|
||||
</div>
|
||||
</Badge>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge badgeType="success">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -37,6 +37,7 @@ import RequestButton from '../RequestButton';
|
||||
import MediaSlider from '../MediaSlider';
|
||||
import ConfirmButton from '../Common/ConfirmButton';
|
||||
import DownloadBlock from '../DownloadBlock';
|
||||
import ButtonWithDropdown from '../Common/ButtonWithDropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
firstAirDate: 'First Air Date',
|
||||
@@ -69,6 +70,8 @@ const messages = defineMessages({
|
||||
opensonarr: 'Open Series in Sonarr',
|
||||
opensonarr4k: 'Open Series in 4K Sonarr',
|
||||
downloadstatus: 'Download Status',
|
||||
playonplex: 'Play on Plex',
|
||||
play4konplex: 'Play 4K on Plex',
|
||||
});
|
||||
|
||||
interface TvDetailsProps {
|
||||
@@ -279,6 +282,8 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
<StatusBadge
|
||||
status={data.mediaInfo?.status}
|
||||
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
|
||||
plexUrl={data.mediaInfo?.plexUrl}
|
||||
plexUrl4k={data.mediaInfo?.plexUrl4k}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
@@ -287,6 +292,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
status={data.mediaInfo?.status4k}
|
||||
is4k
|
||||
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
|
||||
plexUrl={data.mediaInfo?.plexUrl}
|
||||
plexUrl4k={
|
||||
data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_TV))
|
||||
? data.mediaInfo.plexUrl4k
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -303,37 +316,86 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center flex-shrink-0 mt-4 sm:flex-nowrap sm:justify-end lg:mt-0">
|
||||
{trailerUrl && (
|
||||
<a
|
||||
href={trailerUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mb-3 sm:mb-0"
|
||||
{(trailerUrl ||
|
||||
data.mediaInfo?.plexUrl ||
|
||||
data.mediaInfo?.plexUrl4k) && (
|
||||
<ButtonWithDropdown
|
||||
buttonType="ghost"
|
||||
text={
|
||||
<>
|
||||
<svg
|
||||
className="w-5 h-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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{data.mediaInfo?.plexUrl
|
||||
? intl.formatMessage(messages.playonplex)
|
||||
: data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_TV))
|
||||
? intl.formatMessage(messages.play4konplex)
|
||||
: intl.formatMessage(messages.watchtrailer)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
if (data.mediaInfo?.plexUrl) {
|
||||
window.open(data.mediaInfo?.plexUrl, '_blank');
|
||||
} else if (data.mediaInfo?.plexUrl4k) {
|
||||
window.open(data.mediaInfo?.plexUrl4k, '_blank');
|
||||
} else if (trailerUrl) {
|
||||
window.open(trailerUrl, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button buttonType="ghost">
|
||||
<svg
|
||||
className="w-5 h-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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.watchtrailer} />
|
||||
</Button>
|
||||
</a>
|
||||
{data.mediaInfo?.plexUrl ||
|
||||
(data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_TV))) ? (
|
||||
<>
|
||||
{data.mediaInfo?.plexUrl &&
|
||||
data.mediaInfo?.plexUrl4k &&
|
||||
(hasPermission(Permission.REQUEST_4K) ||
|
||||
hasPermission(Permission.REQUEST_4K_TV)) && (
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => {
|
||||
window.open(data.mediaInfo?.plexUrl4k, '_blank');
|
||||
}}
|
||||
buttonType="ghost"
|
||||
>
|
||||
{intl.formatMessage(messages.play4konplex)}
|
||||
</ButtonWithDropdown.Item>
|
||||
)}
|
||||
{(data.mediaInfo?.plexUrl || data.mediaInfo?.plexUrl4k) &&
|
||||
trailerUrl && (
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => {
|
||||
window.open(trailerUrl, '_blank');
|
||||
}}
|
||||
buttonType="ghost"
|
||||
>
|
||||
{intl.formatMessage(messages.watchtrailer)}
|
||||
</ButtonWithDropdown.Item>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</ButtonWithDropdown>
|
||||
)}
|
||||
<div className="mb-3 sm:mb-0">
|
||||
<RequestButton
|
||||
@@ -553,6 +615,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
tmdbId={data.id}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.plexUrl4k}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,8 @@
|
||||
"components.MovieDetails.overview": "Overview",
|
||||
"components.MovieDetails.overviewunavailable": "Overview unavailable.",
|
||||
"components.MovieDetails.pending": "Pending",
|
||||
"components.MovieDetails.play4konplex": "Play 4K on Plex",
|
||||
"components.MovieDetails.playonplex": "Play on Plex",
|
||||
"components.MovieDetails.recommendations": "Recommendations",
|
||||
"components.MovieDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.MovieDetails.releasedate": "Release Date",
|
||||
@@ -513,6 +515,8 @@
|
||||
"components.TvDetails.overview": "Overview",
|
||||
"components.TvDetails.overviewunavailable": "Overview unavailable.",
|
||||
"components.TvDetails.pending": "Pending",
|
||||
"components.TvDetails.play4konplex": "Play 4K on Plex",
|
||||
"components.TvDetails.playonplex": "Play on Plex",
|
||||
"components.TvDetails.recommendations": "Recommendations",
|
||||
"components.TvDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.TvDetails.showtype": "Show Type",
|
||||
|
||||
Reference in New Issue
Block a user