diff --git a/src/components/Common/Badge/index.tsx b/src/components/Common/Badge/index.tsx index 5619eaef..1a2a7a05 100644 --- a/src/components/Common/Badge/index.tsx +++ b/src/components/Common/Badge/index.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import React from 'react'; interface BadgeProps { badgeType?: @@ -14,12 +15,10 @@ interface BadgeProps { children: React.ReactNode; } -const Badge = ({ - badgeType = 'default', - className, - href, - children, -}: BadgeProps) => { +const Badge = ( + { badgeType = 'default', className, href, children }: BadgeProps, + ref?: React.Ref +) => { const badgeStyle = [ 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap', ]; @@ -79,6 +78,7 @@ const Badge = ({ target="_blank" rel="noopener noreferrer" className={badgeStyle.join(' ')} + ref={ref as React.Ref} > {children} @@ -86,12 +86,24 @@ const Badge = ({ } else if (href) { return ( - {children} + } + > + {children} + ); } else { - return {children}; + return ( + } + > + {children} + + ); } }; -export default Badge; +export default React.forwardRef(Badge) as typeof Badge; diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index fc7518f6..c4386c38 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -70,7 +70,7 @@ const ExternalLinkBlock = ({ )} {rtUrl && ( { tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" plexUrl={data.mediaInfo?.plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl} /> {settings.currentSettings.movie4kEnabled && hasPermission( @@ -343,6 +347,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" plexUrl={data.mediaInfo?.plexUrl4k} + serviceUrl={data.mediaInfo?.serviceUrl4k} /> )} @@ -499,36 +504,55 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
{ratingData?.criticsRating && !!ratingData?.criticsScore && ( - <> - + + {ratingData.criticsRating === 'Rotten' ? ( - + ) : ( - + )} - {ratingData.criticsScore}% - - + {ratingData.criticsScore}% + + )} {ratingData?.audienceRating && !!ratingData?.audienceScore && ( - <> - + + {ratingData.audienceRating === 'Spilled' ? ( - + ) : ( - + )} - {ratingData.audienceScore}% - - + {ratingData.audienceScore}% + + )} {!!data.voteCount && ( - <> - - - {data.voteAverage}/10 - - + + + + {Math.round(data.voteAverage * 10)}% + + )}
)} diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index 97889a08..b32c3212 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -1,5 +1,6 @@ import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; +import Tooltip from '@app/components/Common/Tooltip'; import RequestModal from '@app/components/RequestModal'; import useRequestOverride from '@app/hooks/useRequestOverride'; import { useUser } from '@app/hooks/useUser'; @@ -27,6 +28,13 @@ const messages = defineMessages({ profilechanged: 'Quality Profile', rootfolder: 'Root Folder', 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 { @@ -83,7 +91,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
- + + + {
{request.modifiedBy && (
- + + + {
{request.status === MediaRequestStatus.PENDING && ( <> - + + + + + + + + + + + )} + {request.status !== MediaRequestStatus.PENDING && ( + - - - )} - {request.status !== MediaRequestStatus.PENDING && ( - + )}
@@ -187,7 +207,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
- + + + {intl.formatDate(request.createdAt, { year: 'numeric', diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 952f79ba..18b81ac4 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -1,6 +1,7 @@ import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; +import Tooltip from '@app/components/Common/Tooltip'; import RequestModal from '@app/components/RequestModal'; import StatusBadge from '@app/components/StatusBadge'; import { Permission, useUser } from '@app/hooks/useUser'; @@ -31,6 +32,10 @@ const messages = defineMessages({ mediaerror: '{mediaType} Not Found', tmdbid: 'TMDB ID', tvdbid: 'TheTVDB ID', + approverequest: 'Approve Request', + declinerequest: 'Decline Request', + editrequest: 'Edit Request', + cancelrequest: 'Cancel Request', deleterequest: 'Delete Request', }); @@ -139,11 +144,9 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => { : requestData.media.plexUrl } serviceUrl={ - hasPermission(Permission.ADMIN) - ? requestData.is4k - ? requestData.media.serviceUrl4k - : requestData.media.serviceUrl - : undefined + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl } /> )} @@ -153,17 +156,29 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
{hasPermission(Permission.MANAGE_REQUESTS) && requestData?.media.id && ( - + <> + + + + + )}
@@ -389,7 +404,14 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { tmdbId={requestData.media.tmdbId} mediaType={requestData.type} 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 && hasPermission(Permission.MANAGE_REQUESTS) && ( <> - - +
+ + + + +
+
+ + + + +
)} {requestData.status === MediaRequestStatus.PENDING && @@ -442,33 +490,54 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { requestData.requestedBy.id === user?.id && (requestData.type === 'tv' || hasPermission(Permission.REQUEST_ADVANCED)) && ( - +
+ {!hasPermission(Permission.MANAGE_REQUESTS) && ( + + )} + + + +
)} {requestData.status === MediaRequestStatus.PENDING && !hasPermission(Permission.MANAGE_REQUESTS) && requestData.requestedBy.id === user?.id && ( - +
+ + + + +
)} diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 095d1aba..7949a263 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -136,11 +136,9 @@ const RequestItemError = ({ : requestData.media.plexUrl } serviceUrl={ - hasPermission(Permission.ADMIN) - ? requestData.is4k - ? requestData.media.serviceUrl4k - : requestData.media.serviceUrl - : undefined + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl } /> )} @@ -472,9 +470,14 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { tmdbId={requestData.media.tmdbId} mediaType={requestData.type} plexUrl={ - requestData.media[ - requestData.is4k ? 'plexUrl4k' : 'plexUrl' - ] + requestData.is4k + ? requestData.media.plexUrl4k + : requestData.media.plexUrl + } + serviceUrl={ + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl } /> )} diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain.tsx index e8a2063b..3ac7b09b 100644 --- a/src/components/Settings/SettingsMain.tsx +++ b/src/components/Settings/SettingsMain.tsx @@ -46,7 +46,7 @@ const messages = defineMessages({ 'Do NOT enable this setting unless you understand what you are doing!', cacheImages: 'Enable Image Caching', 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', trustProxyTip: 'Allow Overseerr to correctly register client IP addresses behind a proxy', diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index 5929263f..add2bbb8 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -1,5 +1,6 @@ import Spinner from '@app/assets/spinner.svg'; import Badge from '@app/components/Common/Badge'; +import Tooltip from '@app/components/Common/Tooltip'; import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; @@ -9,6 +10,9 @@ import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ status: '{status}', status4k: '4K {status}', + playonplex: 'Play on Plex', + openinarr: 'Open in {arr}', + managemedia: 'Manage {mediaType}', }); interface StatusBadgeProps { @@ -35,6 +39,7 @@ const StatusBadge = ({ const settings = useSettings(); let mediaLink: string | undefined; + let mediaLinkDescription: string | undefined; if ( mediaType && @@ -63,63 +68,94 @@ const StatusBadge = ({ : settings.currentSettings.series4kEnabled)) ) { mediaLink = plexUrl; + mediaLinkDescription = intl.formatMessage(messages.playonplex); } else if (hasPermission(Permission.MANAGE_REQUESTS)) { - mediaLink = - mediaType && tmdbId ? `/${mediaType}/${tmdbId}?manage=1` : serviceUrl; + if (mediaType && tmdbId) { + 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) { case MediaStatus.AVAILABLE: return ( - -
- - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: intl.formatMessage(globalMessages.available), - })} - - {inProgress && } -
-
+ + +
+ + {intl.formatMessage( + is4k ? messages.status4k : messages.status, + { + status: intl.formatMessage(globalMessages.available), + } + )} + + {inProgress && } +
+
+
); case MediaStatus.PARTIALLY_AVAILABLE: return ( - -
- - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: intl.formatMessage(globalMessages.partiallyavailable), - })} - - {inProgress && } -
-
+ + +
+ + {intl.formatMessage( + is4k ? messages.status4k : messages.status, + { + status: intl.formatMessage( + globalMessages.partiallyavailable + ), + } + )} + + {inProgress && } +
+
+
); case MediaStatus.PROCESSING: return ( - -
- - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: inProgress - ? intl.formatMessage(globalMessages.processing) - : intl.formatMessage(globalMessages.requested), - })} - - {inProgress && } -
-
+ + +
+ + {intl.formatMessage( + is4k ? messages.status4k : messages.status, + { + status: inProgress + ? intl.formatMessage(globalMessages.processing) + : intl.formatMessage(globalMessages.requested), + } + )} + + {inProgress && } +
+
+
); case MediaStatus.PENDING: return ( - - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: intl.formatMessage(globalMessages.pending), - })} - + + + {intl.formatMessage(is4k ? messages.status4k : messages.status, { + status: intl.formatMessage(globalMessages.pending), + })} + + ); default: diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 35b995d8..0d16442c 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -79,6 +79,9 @@ const messages = defineMessages({ episodeCount: '{episodeCount, plural, one {# Episode} other {# Episodes}}', seasonnumber: 'Season {seasonNumber}', status4k: '4K {status}', + rtcriticsscore: 'Rotten Tomatoes Tomatometer', + rtaudiencescore: 'Rotten Tomatoes Audience Score', + tmdbuserscore: 'TMDB User Score', }); interface TvDetailsProps { @@ -330,6 +333,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { tmdbId={data.mediaInfo?.tmdbId} mediaType="tv" plexUrl={data.mediaInfo?.plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl} /> {settings.currentSettings.series4kEnabled && hasPermission( @@ -351,6 +355,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { tmdbId={data.mediaInfo?.tmdbId} mediaType="tv" plexUrl={data.mediaInfo?.plexUrl4k} + serviceUrl={data.mediaInfo?.serviceUrl4k} /> )} @@ -660,30 +665,55 @@ const TvDetails = ({ tv }: TvDetailsProps) => { (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
{ratingData?.criticsRating && !!ratingData?.criticsScore && ( - - {ratingData.criticsRating === 'Rotten' ? ( - - ) : ( - - )} - {ratingData.criticsScore}% - + + + {ratingData.criticsRating === 'Rotten' ? ( + + ) : ( + + )} + {ratingData.criticsScore}% + + )} {ratingData?.audienceRating && !!ratingData?.audienceScore && ( - - {ratingData.audienceRating === 'Spilled' ? ( - - ) : ( - - )} - {ratingData.audienceScore}% - + + + {ratingData.audienceRating === 'Spilled' ? ( + + ) : ( + + )} + {ratingData.audienceScore}% + + )} {!!data.voteCount && ( - - - {data.voteAverage}/10 - + + + + {Math.round(data.voteAverage * 10)}% + + )}
)} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index dc8b588c..6c65446f 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -181,6 +181,8 @@ "components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}", "components.MovieDetails.reportissue": "Report an Issue", "components.MovieDetails.revenue": "Revenue", + "components.MovieDetails.rtaudiencescore": "Rotten Tomatoes Audience Score", + "components.MovieDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer", "components.MovieDetails.runtime": "{minutes} minutes", "components.MovieDetails.showless": "Show Less", "components.MovieDetails.showmore": "Show More", @@ -188,6 +190,7 @@ "components.MovieDetails.streamingproviders": "Currently Streaming On", "components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}", "components.MovieDetails.theatricalrelease": "Theatrical Release", + "components.MovieDetails.tmdbuserscore": "TMDB User Score", "components.MovieDetails.viewfullcrew": "View Full Crew", "components.MovieDetails.watchtrailer": "Watch Trailer", "components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.", @@ -292,8 +295,15 @@ "components.QuotaSelector.unlimited": "Unlimited", "components.RegionSelector.regionDefault": "All Regions", "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.lastmodifiedby": "Last Modified By", "components.RequestBlock.profilechanged": "Quality Profile", + "components.RequestBlock.requestdate": "Request Date", + "components.RequestBlock.requestedby": "Requested By", "components.RequestBlock.requestoverrides": "Request Overrides", "components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", @@ -310,7 +320,11 @@ "components.RequestButton.requestmore4k": "Request More in 4K", "components.RequestButton.viewrequest": "View 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.editrequest": "Edit Request", "components.RequestCard.failedretry": "Something went wrong while retrying the request.", "components.RequestCard.mediaerror": "{mediaType} Not Found", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", @@ -736,7 +750,7 @@ "components.Settings.applicationTitle": "Application Title", "components.Settings.applicationurl": "Application URL", "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.copied": "Copied API key to clipboard.", "components.Settings.csrfProtection": "Enable CSRF Protection", @@ -849,6 +863,9 @@ "components.Setup.signinMessage": "Get started by signing in with your Plex account", "components.Setup.tip": "Tip", "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.status4k": "4K {status}", "components.StatusChecker.appUpdated": "{applicationTitle} Updated", @@ -881,6 +898,8 @@ "components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}", "components.TvDetails.recommendations": "Recommendations", "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.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}", "components.TvDetails.seasonstitle": "Seasons", @@ -888,6 +907,7 @@ "components.TvDetails.similar": "Similar Series", "components.TvDetails.status4k": "4K {status}", "components.TvDetails.streamingproviders": "Currently Streaming On", + "components.TvDetails.tmdbuserscore": "TMDB User Score", "components.TvDetails.viewfullcrew": "View Full Crew", "components.TvDetails.watchtrailer": "Watch Trailer", "components.UserList.accounttype": "Type", diff --git a/src/styles/globals.css b/src/styles/globals.css index 203b6a1c..b3e2543e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -184,11 +184,11 @@ } .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 { - @apply mr-4 flex items-center last:mr-0; + @apply flex items-center space-x-1; } .error-message {