feat(frontend): a few more tooltips (#2972)

* feat(frontend): a few more tooltips

* feat: add tooltips to status badges
pull/2973/head
TheCatLady 2 years ago committed by GitHub
parent 8a2acb7f2b
commit 815d709bcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import React from 'react';
interface BadgeProps { interface BadgeProps {
badgeType?: badgeType?:
@ -14,12 +15,10 @@ interface BadgeProps {
children: React.ReactNode; children: React.ReactNode;
} }
const Badge = ({ const Badge = (
badgeType = 'default', { badgeType = 'default', className, href, children }: BadgeProps,
className, ref?: React.Ref<HTMLElement>
href, ) => {
children,
}: BadgeProps) => {
const badgeStyle = [ const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap', 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap',
]; ];
@ -79,6 +78,7 @@ const Badge = ({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={badgeStyle.join(' ')} className={badgeStyle.join(' ')}
ref={ref as React.Ref<HTMLAnchorElement>}
> >
{children} {children}
</a> </a>
@ -86,12 +86,24 @@ const Badge = ({
} else if (href) { } else if (href) {
return ( return (
<Link href={href}> <Link href={href}>
<a className={badgeStyle.join(' ')}>{children}</a> <a
className={badgeStyle.join(' ')}
ref={ref as React.Ref<HTMLAnchorElement>}
>
{children}
</a>
</Link> </Link>
); );
} else { } else {
return <span className={badgeStyle.join(' ')}>{children}</span>; return (
<span
className={badgeStyle.join(' ')}
ref={ref as React.Ref<HTMLSpanElement>}
>
{children}
</span>
);
} }
}; };
export default Badge; export default React.forwardRef(Badge) as typeof Badge;

@ -70,7 +70,7 @@ const ExternalLinkBlock = ({
)} )}
{rtUrl && ( {rtUrl && (
<a <a
href={`${rtUrl}`} href={rtUrl}
className="w-14 opacity-50 transition duration-300 hover:opacity-100" className="w-14 opacity-50 transition duration-300 hover:opacity-100"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"

@ -80,6 +80,9 @@ const messages = defineMessages({
physicalrelease: 'Physical Release', physicalrelease: 'Physical Release',
reportissue: 'Report an Issue', reportissue: 'Report an Issue',
managemovie: 'Manage Movie', managemovie: 'Manage Movie',
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
rtaudiencescore: 'Rotten Tomatoes Audience Score',
tmdbuserscore: 'TMDB User Score',
}); });
interface MovieDetailsProps { interface MovieDetailsProps {
@ -322,6 +325,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie" mediaType="movie"
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl}
/> />
{settings.currentSettings.movie4kEnabled && {settings.currentSettings.movie4kEnabled &&
hasPermission( hasPermission(
@ -343,6 +347,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie" mediaType="movie"
plexUrl={data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
/> />
)} )}
</div> </div>
@ -499,36 +504,55 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
(ratingData?.audienceRating && !!ratingData?.audienceScore)) && ( (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
<div className="media-ratings"> <div className="media-ratings">
{ratingData?.criticsRating && !!ratingData?.criticsScore && ( {ratingData?.criticsRating && !!ratingData?.criticsScore && (
<> <Tooltip
<span className="media-rating"> content={intl.formatMessage(messages.rtcriticsscore)}
>
<a
href={ratingData.url}
className="media-rating"
target="_blank"
rel="noreferrer"
>
{ratingData.criticsRating === 'Rotten' ? ( {ratingData.criticsRating === 'Rotten' ? (
<RTRotten className="mr-1 w-6" /> <RTRotten className="w-6" />
) : ( ) : (
<RTFresh className="mr-1 w-6" /> <RTFresh className="w-6" />
)} )}
{ratingData.criticsScore}% <span>{ratingData.criticsScore}%</span>
</span> </a>
</> </Tooltip>
)} )}
{ratingData?.audienceRating && !!ratingData?.audienceScore && ( {ratingData?.audienceRating && !!ratingData?.audienceScore && (
<> <Tooltip
<span className="media-rating"> content={intl.formatMessage(messages.rtaudiencescore)}
>
<a
href={ratingData.url}
className="media-rating"
target="_blank"
rel="noreferrer"
>
{ratingData.audienceRating === 'Spilled' ? ( {ratingData.audienceRating === 'Spilled' ? (
<RTAudRotten className="mr-1 w-6" /> <RTAudRotten className="w-6" />
) : ( ) : (
<RTAudFresh className="mr-1 w-6" /> <RTAudFresh className="w-6" />
)} )}
{ratingData.audienceScore}% <span>{ratingData.audienceScore}%</span>
</span> </a>
</> </Tooltip>
)} )}
{!!data.voteCount && ( {!!data.voteCount && (
<> <Tooltip content={intl.formatMessage(messages.tmdbuserscore)}>
<span className="media-rating"> <a
<TmdbLogo className="mr-2 w-6" /> href={`https://www.themoviedb.org/movie/${data.id}?language=${locale}`}
{data.voteAverage}/10 className="media-rating"
</span> target="_blank"
</> rel="noreferrer"
>
<TmdbLogo className="mr-1 w-6" />
<span>{Math.round(data.voteAverage * 10)}%</span>
</a>
</Tooltip>
)} )}
</div> </div>
)} )}

@ -1,5 +1,6 @@
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import useRequestOverride from '@app/hooks/useRequestOverride'; import useRequestOverride from '@app/hooks/useRequestOverride';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
@ -27,6 +28,13 @@ const messages = defineMessages({
profilechanged: 'Quality Profile', profilechanged: 'Quality Profile',
rootfolder: 'Root Folder', rootfolder: 'Root Folder',
languageprofile: 'Language Profile', languageprofile: 'Language Profile',
requestdate: 'Request Date',
requestedby: 'Requested By',
lastmodifiedby: 'Last Modified By',
approve: 'Approve Request',
decline: 'Decline Request',
edit: 'Edit Request',
delete: 'Delete Request',
}); });
interface RequestBlockProps { interface RequestBlockProps {
@ -83,7 +91,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5"> <div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
<div className="white mb-1 flex flex-nowrap"> <div className="white mb-1 flex flex-nowrap">
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" /> <Tooltip content={intl.formatMessage(messages.requestedby)}>
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
<Link <Link
href={ href={
@ -100,7 +110,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
</div> </div>
{request.modifiedBy && ( {request.modifiedBy && (
<div className="flex flex-nowrap"> <div className="flex flex-nowrap">
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" /> <Tooltip content={intl.formatMessage(messages.lastmodifiedby)}>
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
<Link <Link
href={ href={
@ -120,39 +132,47 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
<div className="ml-2 flex flex-shrink-0 flex-wrap"> <div className="ml-2 flex flex-shrink-0 flex-wrap">
{request.status === MediaRequestStatus.PENDING && ( {request.status === MediaRequestStatus.PENDING && (
<> <>
<Button <Tooltip content={intl.formatMessage(messages.approve)}>
buttonType="success" <Button
className="mr-1" buttonType="success"
onClick={() => updateRequest('approve')} className="mr-1"
disabled={isUpdating} onClick={() => updateRequest('approve')}
> disabled={isUpdating}
<CheckIcon className="icon-sm" /> >
</Button> <CheckIcon className="icon-sm" />
</Button>
</Tooltip>
<Tooltip content={intl.formatMessage(messages.decline)}>
<Button
buttonType="danger"
className="mr-1"
onClick={() => updateRequest('decline')}
disabled={isUpdating}
>
<XIcon />
</Button>
</Tooltip>
<Tooltip content={intl.formatMessage(messages.edit)}>
<Button
buttonType="primary"
onClick={() => setShowEditModal(true)}
disabled={isUpdating}
>
<PencilIcon className="icon-sm" />
</Button>
</Tooltip>
</>
)}
{request.status !== MediaRequestStatus.PENDING && (
<Tooltip content={intl.formatMessage(messages.delete)}>
<Button <Button
buttonType="danger" buttonType="danger"
className="mr-1" onClick={() => deleteRequest()}
onClick={() => updateRequest('decline')}
disabled={isUpdating}
>
<XIcon />
</Button>
<Button
buttonType="primary"
onClick={() => setShowEditModal(true)}
disabled={isUpdating} disabled={isUpdating}
> >
<PencilIcon className="icon-sm" /> <TrashIcon className="icon-sm" />
</Button> </Button>
</> </Tooltip>
)}
{request.status !== MediaRequestStatus.PENDING && (
<Button
buttonType="danger"
onClick={() => deleteRequest()}
disabled={isUpdating}
>
<TrashIcon className="icon-sm" />
</Button>
)} )}
</div> </div>
</div> </div>
@ -187,7 +207,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
</div> </div>
</div> </div>
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0"> <div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
<CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" /> <Tooltip content={intl.formatMessage(messages.requestdate)}>
<CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span> <span>
{intl.formatDate(request.createdAt, { {intl.formatDate(request.createdAt, {
year: 'numeric', year: 'numeric',

@ -1,6 +1,7 @@
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
@ -31,6 +32,10 @@ const messages = defineMessages({
mediaerror: '{mediaType} Not Found', mediaerror: '{mediaType} Not Found',
tmdbid: 'TMDB ID', tmdbid: 'TMDB ID',
tvdbid: 'TheTVDB ID', tvdbid: 'TheTVDB ID',
approverequest: 'Approve Request',
declinerequest: 'Decline Request',
editrequest: 'Edit Request',
cancelrequest: 'Cancel Request',
deleterequest: 'Delete Request', deleterequest: 'Delete Request',
}); });
@ -139,11 +144,9 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
: requestData.media.plexUrl : requestData.media.plexUrl
} }
serviceUrl={ serviceUrl={
hasPermission(Permission.ADMIN) requestData.is4k
? requestData.is4k ? requestData.media.serviceUrl4k
? requestData.media.serviceUrl4k : requestData.media.serviceUrl
: requestData.media.serviceUrl
: undefined
} }
/> />
)} )}
@ -153,17 +156,29 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
<div className="flex flex-1 items-end space-x-2"> <div className="flex flex-1 items-end space-x-2">
{hasPermission(Permission.MANAGE_REQUESTS) && {hasPermission(Permission.MANAGE_REQUESTS) &&
requestData?.media.id && ( requestData?.media.id && (
<Button <>
buttonType="danger" <Button
buttonSize="sm" buttonType="danger"
className="mt-4" buttonSize="sm"
onClick={() => deleteRequest()} className="mt-4 hidden sm:block"
> onClick={() => deleteRequest()}
<TrashIcon style={{ marginRight: '0' }} /> >
<span className="ml-1.5 hidden sm:block"> <TrashIcon />
{intl.formatMessage(messages.deleterequest)} <span>{intl.formatMessage(globalMessages.delete)}</span>
</span> </Button>
</Button> <Tooltip
content={intl.formatMessage(messages.deleterequest)}
>
<Button
buttonType="danger"
buttonSize="sm"
className="mt-4 sm:hidden"
onClick={() => deleteRequest()}
>
<TrashIcon />
</Button>
</Tooltip>
</>
)} )}
</div> </div>
</div> </div>
@ -389,7 +404,14 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
tmdbId={requestData.media.tmdbId} tmdbId={requestData.media.tmdbId}
mediaType={requestData.type} mediaType={requestData.type}
plexUrl={ plexUrl={
requestData.media[requestData.is4k ? 'plexUrl4k' : 'plexUrl'] requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
: requestData.media.serviceUrl
} }
/> />
)} )}
@ -415,26 +437,52 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<> <>
<Button <div>
buttonType="success" <Button
buttonSize="sm" buttonType="success"
onClick={() => modifyRequest('approve')} buttonSize="sm"
> className="hidden sm:block"
<CheckIcon style={{ marginRight: '0' }} /> onClick={() => modifyRequest('approve')}
<span className="ml-1.5 hidden sm:block"> >
{intl.formatMessage(globalMessages.approve)} <CheckIcon />
</span> <span>{intl.formatMessage(globalMessages.approve)}</span>
</Button> </Button>
<Button <Tooltip
buttonType="danger" content={intl.formatMessage(messages.approverequest)}
buttonSize="sm" >
onClick={() => modifyRequest('decline')} <Button
> buttonType="success"
<XIcon style={{ marginRight: '0' }} /> buttonSize="sm"
<span className="ml-1.5 hidden sm:block"> className="sm:hidden"
{intl.formatMessage(globalMessages.decline)} onClick={() => modifyRequest('approve')}
</span> >
</Button> <CheckIcon />
</Button>
</Tooltip>
</div>
<div>
<Button
buttonType="danger"
buttonSize="sm"
className="hidden sm:block"
onClick={() => modifyRequest('decline')}
>
<XIcon />
<span>{intl.formatMessage(globalMessages.decline)}</span>
</Button>
<Tooltip
content={intl.formatMessage(messages.declinerequest)}
>
<Button
buttonType="danger"
buttonSize="sm"
className="sm:hidden"
onClick={() => modifyRequest('decline')}
>
<XIcon />
</Button>
</Tooltip>
</div>
</> </>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
@ -442,33 +490,54 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
requestData.requestedBy.id === user?.id && requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' || (requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)) && ( hasPermission(Permission.REQUEST_ADVANCED)) && (
<Button <div>
buttonType="primary" {!hasPermission(Permission.MANAGE_REQUESTS) && (
buttonSize="sm" <Button
onClick={() => setShowEditModal(true)} buttonType="primary"
className={`${ buttonSize="sm"
hasPermission(Permission.MANAGE_REQUESTS) ? 'sm:hidden' : '' className="hidden sm:block"
}`} onClick={() => setShowEditModal(true)}
> >
<PencilIcon style={{ marginRight: '0' }} /> <PencilIcon />
<span className="ml-1.5 hidden sm:block"> <span>{intl.formatMessage(globalMessages.edit)}</span>
{intl.formatMessage(globalMessages.edit)} </Button>
</span> )}
</Button> <Tooltip content={intl.formatMessage(messages.editrequest)}>
<Button
buttonType="primary"
buttonSize="sm"
className="sm:hidden"
onClick={() => setShowEditModal(true)}
>
<PencilIcon />
</Button>
</Tooltip>
</div>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) && !hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && ( requestData.requestedBy.id === user?.id && (
<Button <div>
buttonType="danger" <Button
buttonSize="sm" buttonType="danger"
onClick={() => deleteRequest()} buttonSize="sm"
> className="hidden sm:block"
<XIcon style={{ marginRight: '0' }} /> onClick={() => deleteRequest()}
<span className="ml-1.5 hidden sm:block"> >
{intl.formatMessage(globalMessages.cancel)} <XIcon />
</span> <span>{intl.formatMessage(globalMessages.cancel)}</span>
</Button> </Button>
<Tooltip content={intl.formatMessage(messages.cancelrequest)}>
<Button
buttonType="danger"
buttonSize="sm"
className="sm:hidden"
onClick={() => deleteRequest()}
>
<XIcon />
</Button>
</Tooltip>
</div>
)} )}
</div> </div>
</div> </div>

@ -136,11 +136,9 @@ const RequestItemError = ({
: requestData.media.plexUrl : requestData.media.plexUrl
} }
serviceUrl={ serviceUrl={
hasPermission(Permission.ADMIN) requestData.is4k
? requestData.is4k ? requestData.media.serviceUrl4k
? requestData.media.serviceUrl4k : requestData.media.serviceUrl
: requestData.media.serviceUrl
: undefined
} }
/> />
)} )}
@ -472,9 +470,14 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
tmdbId={requestData.media.tmdbId} tmdbId={requestData.media.tmdbId}
mediaType={requestData.type} mediaType={requestData.type}
plexUrl={ plexUrl={
requestData.media[ requestData.is4k
requestData.is4k ? 'plexUrl4k' : 'plexUrl' ? requestData.media.plexUrl4k
] : requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
: requestData.media.serviceUrl
} }
/> />
)} )}

@ -46,7 +46,7 @@ const messages = defineMessages({
'Do NOT enable this setting unless you understand what you are doing!', 'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching', cacheImages: 'Enable Image Caching',
cacheImagesTip: cacheImagesTip:
'Optimize and store all images locally (consumes a significant amount of disk space)', 'Cache and serve optimized images (requires a significant amount of disk space)',
trustProxy: 'Enable Proxy Support', trustProxy: 'Enable Proxy Support',
trustProxyTip: trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy', 'Allow Overseerr to correctly register client IP addresses behind a proxy',

@ -1,5 +1,6 @@
import Spinner from '@app/assets/spinner.svg'; import Spinner from '@app/assets/spinner.svg';
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import Tooltip from '@app/components/Common/Tooltip';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
@ -9,6 +10,9 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
status: '{status}', status: '{status}',
status4k: '4K {status}', status4k: '4K {status}',
playonplex: 'Play on Plex',
openinarr: 'Open in {arr}',
managemedia: 'Manage {mediaType}',
}); });
interface StatusBadgeProps { interface StatusBadgeProps {
@ -35,6 +39,7 @@ const StatusBadge = ({
const settings = useSettings(); const settings = useSettings();
let mediaLink: string | undefined; let mediaLink: string | undefined;
let mediaLinkDescription: string | undefined;
if ( if (
mediaType && mediaType &&
@ -63,63 +68,94 @@ const StatusBadge = ({
: settings.currentSettings.series4kEnabled)) : settings.currentSettings.series4kEnabled))
) { ) {
mediaLink = plexUrl; mediaLink = plexUrl;
mediaLinkDescription = intl.formatMessage(messages.playonplex);
} else if (hasPermission(Permission.MANAGE_REQUESTS)) { } else if (hasPermission(Permission.MANAGE_REQUESTS)) {
mediaLink = if (mediaType && tmdbId) {
mediaType && tmdbId ? `/${mediaType}/${tmdbId}?manage=1` : serviceUrl; mediaLink = `/${mediaType}/${tmdbId}?manage=1`;
mediaLinkDescription = intl.formatMessage(messages.managemedia, {
mediaType: intl.formatMessage(
mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow
),
});
} else if (hasPermission(Permission.ADMIN)) {
mediaLink = serviceUrl;
mediaLinkDescription = intl.formatMessage(messages.openinarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
});
}
} }
switch (status) { switch (status) {
case MediaStatus.AVAILABLE: case MediaStatus.AVAILABLE:
return ( return (
<Badge badgeType="success" href={mediaLink}> <Tooltip content={mediaLinkDescription}>
<div className="flex items-center"> <Badge badgeType="success" href={mediaLink}>
<span> <div className="flex items-center">
{intl.formatMessage(is4k ? messages.status4k : messages.status, { <span>
status: intl.formatMessage(globalMessages.available), {intl.formatMessage(
})} is4k ? messages.status4k : messages.status,
</span> {
{inProgress && <Spinner className="ml-1 h-3 w-3" />} status: intl.formatMessage(globalMessages.available),
</div> }
</Badge> )}
</span>
{inProgress && <Spinner className="ml-1 h-3 w-3" />}
</div>
</Badge>
</Tooltip>
); );
case MediaStatus.PARTIALLY_AVAILABLE: case MediaStatus.PARTIALLY_AVAILABLE:
return ( return (
<Badge badgeType="success" href={mediaLink}> <Tooltip content={mediaLinkDescription}>
<div className="flex items-center"> <Badge badgeType="success" href={mediaLink}>
<span> <div className="flex items-center">
{intl.formatMessage(is4k ? messages.status4k : messages.status, { <span>
status: intl.formatMessage(globalMessages.partiallyavailable), {intl.formatMessage(
})} is4k ? messages.status4k : messages.status,
</span> {
{inProgress && <Spinner className="ml-1 h-3 w-3" />} status: intl.formatMessage(
</div> globalMessages.partiallyavailable
</Badge> ),
}
)}
</span>
{inProgress && <Spinner className="ml-1 h-3 w-3" />}
</div>
</Badge>
</Tooltip>
); );
case MediaStatus.PROCESSING: case MediaStatus.PROCESSING:
return ( return (
<Badge badgeType="primary" href={mediaLink}> <Tooltip content={mediaLinkDescription}>
<div className="flex items-center"> <Badge badgeType="primary" href={mediaLink}>
<span> <div className="flex items-center">
{intl.formatMessage(is4k ? messages.status4k : messages.status, { <span>
status: inProgress {intl.formatMessage(
? intl.formatMessage(globalMessages.processing) is4k ? messages.status4k : messages.status,
: intl.formatMessage(globalMessages.requested), {
})} status: inProgress
</span> ? intl.formatMessage(globalMessages.processing)
{inProgress && <Spinner className="ml-1 h-3 w-3" />} : intl.formatMessage(globalMessages.requested),
</div> }
</Badge> )}
</span>
{inProgress && <Spinner className="ml-1 h-3 w-3" />}
</div>
</Badge>
</Tooltip>
); );
case MediaStatus.PENDING: case MediaStatus.PENDING:
return ( return (
<Badge badgeType="warning" href={mediaLink}> <Tooltip content={mediaLinkDescription}>
{intl.formatMessage(is4k ? messages.status4k : messages.status, { <Badge badgeType="warning" href={mediaLink}>
status: intl.formatMessage(globalMessages.pending), {intl.formatMessage(is4k ? messages.status4k : messages.status, {
})} status: intl.formatMessage(globalMessages.pending),
</Badge> })}
</Badge>
</Tooltip>
); );
default: default:

@ -79,6 +79,9 @@ const messages = defineMessages({
episodeCount: '{episodeCount, plural, one {# Episode} other {# Episodes}}', episodeCount: '{episodeCount, plural, one {# Episode} other {# Episodes}}',
seasonnumber: 'Season {seasonNumber}', seasonnumber: 'Season {seasonNumber}',
status4k: '4K {status}', status4k: '4K {status}',
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
rtaudiencescore: 'Rotten Tomatoes Audience Score',
tmdbuserscore: 'TMDB User Score',
}); });
interface TvDetailsProps { interface TvDetailsProps {
@ -330,6 +333,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv" mediaType="tv"
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl}
/> />
{settings.currentSettings.series4kEnabled && {settings.currentSettings.series4kEnabled &&
hasPermission( hasPermission(
@ -351,6 +355,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv" mediaType="tv"
plexUrl={data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
/> />
)} )}
</div> </div>
@ -660,30 +665,55 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
(ratingData?.audienceRating && !!ratingData?.audienceScore)) && ( (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
<div className="media-ratings"> <div className="media-ratings">
{ratingData?.criticsRating && !!ratingData?.criticsScore && ( {ratingData?.criticsRating && !!ratingData?.criticsScore && (
<span className="media-rating"> <Tooltip
{ratingData.criticsRating === 'Rotten' ? ( content={intl.formatMessage(messages.rtcriticsscore)}
<RTRotten className="mr-1 w-6" /> >
) : ( <a
<RTFresh className="mr-1 w-6" /> href={ratingData.url}
)} className="media-rating"
{ratingData.criticsScore}% target="_blank"
</span> rel="noreferrer"
>
{ratingData.criticsRating === 'Rotten' ? (
<RTRotten className="mr-1 w-6" />
) : (
<RTFresh className="mr-1 w-6" />
)}
<span>{ratingData.criticsScore}%</span>
</a>
</Tooltip>
)} )}
{ratingData?.audienceRating && !!ratingData?.audienceScore && ( {ratingData?.audienceRating && !!ratingData?.audienceScore && (
<span className="media-rating"> <Tooltip
{ratingData.audienceRating === 'Spilled' ? ( content={intl.formatMessage(messages.rtaudiencescore)}
<RTAudRotten className="mr-1 w-6" /> >
) : ( <a
<RTAudFresh className="mr-1 w-6" /> href={ratingData.url}
)} className="media-rating"
{ratingData.audienceScore}% target="_blank"
</span> rel="noreferrer"
>
{ratingData.audienceRating === 'Spilled' ? (
<RTAudRotten className="mr-1 w-6" />
) : (
<RTAudFresh className="mr-1 w-6" />
)}
<span>{ratingData.audienceScore}%</span>
</a>
</Tooltip>
)} )}
{!!data.voteCount && ( {!!data.voteCount && (
<span className="media-rating"> <Tooltip content={intl.formatMessage(messages.tmdbuserscore)}>
<TmdbLogo className="mr-2 w-6" /> <a
{data.voteAverage}/10 href={`https://www.themoviedb.org/tv/${data.id}?language=${locale}`}
</span> className="media-rating"
target="_blank"
rel="noreferrer"
>
<TmdbLogo className="mr-1 w-6" />
<span>{Math.round(data.voteAverage * 10)}%</span>
</a>
</Tooltip>
)} )}
</div> </div>
)} )}

@ -181,6 +181,8 @@
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}", "components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
"components.MovieDetails.reportissue": "Report an Issue", "components.MovieDetails.reportissue": "Report an Issue",
"components.MovieDetails.revenue": "Revenue", "components.MovieDetails.revenue": "Revenue",
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
"components.MovieDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
"components.MovieDetails.runtime": "{minutes} minutes", "components.MovieDetails.runtime": "{minutes} minutes",
"components.MovieDetails.showless": "Show Less", "components.MovieDetails.showless": "Show Less",
"components.MovieDetails.showmore": "Show More", "components.MovieDetails.showmore": "Show More",
@ -188,6 +190,7 @@
"components.MovieDetails.streamingproviders": "Currently Streaming On", "components.MovieDetails.streamingproviders": "Currently Streaming On",
"components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}", "components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}",
"components.MovieDetails.theatricalrelease": "Theatrical Release", "components.MovieDetails.theatricalrelease": "Theatrical Release",
"components.MovieDetails.tmdbuserscore": "TMDB User Score",
"components.MovieDetails.viewfullcrew": "View Full Crew", "components.MovieDetails.viewfullcrew": "View Full Crew",
"components.MovieDetails.watchtrailer": "Watch Trailer", "components.MovieDetails.watchtrailer": "Watch Trailer",
"components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.", "components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.",
@ -292,8 +295,15 @@
"components.QuotaSelector.unlimited": "Unlimited", "components.QuotaSelector.unlimited": "Unlimited",
"components.RegionSelector.regionDefault": "All Regions", "components.RegionSelector.regionDefault": "All Regions",
"components.RegionSelector.regionServerDefault": "Default ({region})", "components.RegionSelector.regionServerDefault": "Default ({region})",
"components.RequestBlock.approve": "Approve Request",
"components.RequestBlock.decline": "Decline Request",
"components.RequestBlock.delete": "Delete Request",
"components.RequestBlock.edit": "Edit Request",
"components.RequestBlock.languageprofile": "Language Profile", "components.RequestBlock.languageprofile": "Language Profile",
"components.RequestBlock.lastmodifiedby": "Last Modified By",
"components.RequestBlock.profilechanged": "Quality Profile", "components.RequestBlock.profilechanged": "Quality Profile",
"components.RequestBlock.requestdate": "Request Date",
"components.RequestBlock.requestedby": "Requested By",
"components.RequestBlock.requestoverrides": "Request Overrides", "components.RequestBlock.requestoverrides": "Request Overrides",
"components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.rootfolder": "Root Folder",
"components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
@ -310,7 +320,11 @@
"components.RequestButton.requestmore4k": "Request More in 4K", "components.RequestButton.requestmore4k": "Request More in 4K",
"components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest": "View Request",
"components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestButton.viewrequest4k": "View 4K Request",
"components.RequestCard.approverequest": "Approve Request",
"components.RequestCard.cancelrequest": "Cancel Request",
"components.RequestCard.declinerequest": "Decline Request",
"components.RequestCard.deleterequest": "Delete Request", "components.RequestCard.deleterequest": "Delete Request",
"components.RequestCard.editrequest": "Edit Request",
"components.RequestCard.failedretry": "Something went wrong while retrying the request.", "components.RequestCard.failedretry": "Something went wrong while retrying the request.",
"components.RequestCard.mediaerror": "{mediaType} Not Found", "components.RequestCard.mediaerror": "{mediaType} Not Found",
"components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
@ -736,7 +750,7 @@
"components.Settings.applicationTitle": "Application Title", "components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL", "components.Settings.applicationurl": "Application URL",
"components.Settings.cacheImages": "Enable Image Caching", "components.Settings.cacheImages": "Enable Image Caching",
"components.Settings.cacheImagesTip": "Optimize and store all images locally (consumes a significant amount of disk space)", "components.Settings.cacheImagesTip": "Cache and serve optimized images (requires a significant amount of disk space)",
"components.Settings.cancelscan": "Cancel Scan", "components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.", "components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection", "components.Settings.csrfProtection": "Enable CSRF Protection",
@ -849,6 +863,9 @@
"components.Setup.signinMessage": "Get started by signing in with your Plex account", "components.Setup.signinMessage": "Get started by signing in with your Plex account",
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Overseerr", "components.Setup.welcome": "Welcome to Overseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}",
"components.StatusBadge.openinarr": "Open in {arr}",
"components.StatusBadge.playonplex": "Play on Plex",
"components.StatusBadge.status": "{status}", "components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusChecker.appUpdated": "{applicationTitle} Updated", "components.StatusChecker.appUpdated": "{applicationTitle} Updated",
@ -881,6 +898,8 @@
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}", "components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
"components.TvDetails.recommendations": "Recommendations", "components.TvDetails.recommendations": "Recommendations",
"components.TvDetails.reportissue": "Report an Issue", "components.TvDetails.reportissue": "Report an Issue",
"components.TvDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
"components.TvDetails.seasonnumber": "Season {seasonNumber}", "components.TvDetails.seasonnumber": "Season {seasonNumber}",
"components.TvDetails.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}", "components.TvDetails.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}",
"components.TvDetails.seasonstitle": "Seasons", "components.TvDetails.seasonstitle": "Seasons",
@ -888,6 +907,7 @@
"components.TvDetails.similar": "Similar Series", "components.TvDetails.similar": "Similar Series",
"components.TvDetails.status4k": "4K {status}", "components.TvDetails.status4k": "4K {status}",
"components.TvDetails.streamingproviders": "Currently Streaming On", "components.TvDetails.streamingproviders": "Currently Streaming On",
"components.TvDetails.tmdbuserscore": "TMDB User Score",
"components.TvDetails.viewfullcrew": "View Full Crew", "components.TvDetails.viewfullcrew": "View Full Crew",
"components.TvDetails.watchtrailer": "Watch Trailer", "components.TvDetails.watchtrailer": "Watch Trailer",
"components.UserList.accounttype": "Type", "components.UserList.accounttype": "Type",

@ -184,11 +184,11 @@
} }
.media-ratings { .media-ratings {
@apply flex items-center justify-center border-b border-gray-700 px-4 py-2 font-medium last:border-b-0; @apply flex items-center justify-center space-x-5 border-b border-gray-700 px-4 py-2 font-medium last:border-b-0;
} }
.media-rating { .media-rating {
@apply mr-4 flex items-center last:mr-0; @apply flex items-center space-x-1;
} }
.error-message { .error-message {

Loading…
Cancel
Save