feat(ui): link processing/requested status badges to service URL (#1761)

* feat(ui): link processing/requested status badges to service URL where available

* refactor: add URL prop to Badge component

* fix(css): tweak font weight of media rating values and request card link hover effect

* fix: only set StatusBadge serviceUrl for admins
This commit is contained in:
TheCatLady
2021-10-16 11:53:38 -04:00
committed by GitHub
parent 084a842a4f
commit 032c14a226
8 changed files with 124 additions and 145 deletions

View File

@@ -3,36 +3,64 @@ import React from 'react';
interface BadgeProps { interface BadgeProps {
badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success'; badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success';
className?: string; className?: string;
url?: string;
} }
const Badge: React.FC<BadgeProps> = ({ const Badge: React.FC<BadgeProps> = ({
badgeType = 'default', badgeType = 'default',
className, className,
url,
children, children,
}) => { }) => {
const badgeStyle = [ const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full cursor-default', 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full',
]; ];
if (url) {
badgeStyle.push('transition cursor-pointer');
} else {
badgeStyle.push('cursor-default');
}
switch (badgeType) { switch (badgeType) {
case 'danger': case 'danger':
badgeStyle.push('bg-red-600 text-red-100'); badgeStyle.push('bg-red-600 text-red-100');
if (url) {
badgeStyle.push('hover:bg-red-500');
}
break; break;
case 'warning': case 'warning':
badgeStyle.push('bg-yellow-500 text-yellow-100'); badgeStyle.push('bg-yellow-500 text-yellow-100');
if (url) {
badgeStyle.push('hover:bg-yellow-400');
}
break; break;
case 'success': case 'success':
badgeStyle.push('bg-green-500 text-green-100'); badgeStyle.push('bg-green-500 text-green-100');
if (url) {
badgeStyle.push('hover:bg-green-400');
}
break; break;
default: default:
badgeStyle.push('bg-indigo-500 text-indigo-100'); badgeStyle.push('bg-indigo-500 text-indigo-100');
if (url) {
badgeStyle.push('hover:bg-indigo-400');
}
} }
if (className) { if (className) {
badgeStyle.push(className); badgeStyle.push(className);
} }
return <span className={badgeStyle.join(' ')}>{children}</span>; if (url) {
return (
<a href={url} target="_blank" rel="noopener noreferrer">
<span className={badgeStyle.join(' ')}>{children}</span>
</a>
);
} else {
return <span className={badgeStyle.join(' ')}>{children}</span>;
}
}; };
export default Badge; export default Badge;

View File

@@ -420,6 +420,11 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
status={data.mediaInfo?.status} status={data.mediaInfo?.status}
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={
hasPermission(Permission.ADMIN)
? data.mediaInfo?.serviceUrl
: undefined
}
/> />
{settings.currentSettings.movie4kEnabled && {settings.currentSettings.movie4kEnabled &&
hasPermission( hasPermission(
@@ -434,7 +439,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
inProgress={ inProgress={
(data.mediaInfo?.downloadStatus4k ?? []).length > 0 (data.mediaInfo?.downloadStatus4k ?? []).length > 0
} }
plexUrl4k={data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={
hasPermission(Permission.ADMIN)
? data.mediaInfo?.serviceUrl4k
: undefined
}
/> />
)} )}
</div> </div>

View File

@@ -292,8 +292,18 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
).length > 0 ).length > 0
} }
is4k={requestData.is4k} is4k={requestData.is4k}
plexUrl={requestData.media.plexUrl} plexUrl={
plexUrl4k={requestData.media.plexUrl4k} requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
hasPermission(Permission.ADMIN)
? requestData.is4k
? requestData.media.serviceUrl4k
: requestData.media.serviceUrl
: undefined
}
/> />
)} )}
</div> </div>

View File

@@ -294,8 +294,18 @@ const RequestItem: React.FC<RequestItemProps> = ({
).length > 0 ).length > 0
} }
is4k={requestData.is4k} is4k={requestData.is4k}
plexUrl={requestData.media.plexUrl} plexUrl={
plexUrl4k={requestData.media.plexUrl4k} requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
hasPermission(Permission.ADMIN)
? requestData.is4k
? requestData.media.serviceUrl4k
: requestData.media.serviceUrl
: undefined
}
/> />
)} )}
</div> </div>

View File

@@ -6,6 +6,7 @@ import globalMessages from '../../i18n/globalMessages';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
const messages = defineMessages({ const messages = defineMessages({
status: '{status}',
status4k: '4K {status}', status4k: '4K {status}',
}); });
@@ -14,7 +15,7 @@ interface StatusBadgeProps {
is4k?: boolean; is4k?: boolean;
inProgress?: boolean; inProgress?: boolean;
plexUrl?: string; plexUrl?: string;
plexUrl4k?: string; serviceUrl?: string;
} }
const StatusBadge: React.FC<StatusBadgeProps> = ({ const StatusBadge: React.FC<StatusBadgeProps> = ({
@@ -22,158 +23,64 @@ const StatusBadge: React.FC<StatusBadgeProps> = ({
is4k = false, is4k = false,
inProgress = false, inProgress = false,
plexUrl, plexUrl,
plexUrl4k, serviceUrl,
}) => { }) => {
const intl = useIntl(); 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, {
status: intl.formatMessage(globalMessages.available),
})}
</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, {
status: intl.formatMessage(globalMessages.partiallyavailable),
})}
</Badge>
);
case MediaStatus.PROCESSING:
return (
<Badge badgeType="primary">
<div className="flex items-center">
<span>
{intl.formatMessage(messages.status4k, {
status: inProgress
? intl.formatMessage(globalMessages.processing)
: intl.formatMessage(globalMessages.requested),
})}
</span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div>
</Badge>
);
case MediaStatus.PENDING:
return (
<Badge badgeType="warning">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(globalMessages.pending),
})}
</Badge>
);
default:
return null;
}
}
switch (status) { switch (status) {
case MediaStatus.AVAILABLE: 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 ( return (
<Badge badgeType="success"> <Badge badgeType="success" url={plexUrl}>
<div className="flex items-center">
<span>{intl.formatMessage(globalMessages.available)}</span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div>
</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">
<span>{intl.formatMessage(globalMessages.partiallyavailable)}</span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div>
</Badge>
);
case MediaStatus.PROCESSING:
return (
<Badge badgeType="primary">
<div className="flex items-center"> <div className="flex items-center">
<span> <span>
{inProgress {intl.formatMessage(is4k ? messages.status4k : messages.status, {
? intl.formatMessage(globalMessages.processing) status: intl.formatMessage(globalMessages.available),
: intl.formatMessage(globalMessages.requested)} })}
</span> </span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />} {inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div> </div>
</Badge> </Badge>
); );
case MediaStatus.PARTIALLY_AVAILABLE:
return (
<Badge badgeType="success" url={plexUrl}>
<div className="flex items-center">
<span>
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.partiallyavailable),
})}
</span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div>
</Badge>
);
case MediaStatus.PROCESSING:
return (
<Badge badgeType="primary" url={serviceUrl}>
<div className="flex items-center">
<span>
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: inProgress
? intl.formatMessage(globalMessages.processing)
: intl.formatMessage(globalMessages.requested),
})}
</span>
{inProgress && <Spinner className="w-3 h-3 ml-1" />}
</div>
</Badge>
);
case MediaStatus.PENDING: case MediaStatus.PENDING:
return ( return (
<Badge badgeType="warning"> <Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)} {intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.pending),
})}
</Badge> </Badge>
); );
default: default:
return null; return null;
} }

View File

@@ -430,6 +430,11 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
status={data.mediaInfo?.status} status={data.mediaInfo?.status}
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl} plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={
hasPermission(Permission.ADMIN)
? data.mediaInfo?.serviceUrl
: undefined
}
/> />
{settings.currentSettings.series4kEnabled && {settings.currentSettings.series4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
@@ -441,7 +446,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
inProgress={ inProgress={
(data.mediaInfo?.downloadStatus4k ?? []).length > 0 (data.mediaInfo?.downloadStatus4k ?? []).length > 0
} }
plexUrl4k={data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={
hasPermission(Permission.ADMIN)
? data.mediaInfo?.serviceUrl4k
: undefined
}
/> />
)} )}
</div> </div>

View File

@@ -673,6 +673,7 @@
"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.status": "{status}",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Overseerr has been updated! Please click the button below to reload the page.", "components.StatusChacker.newversionDescription": "Overseerr has been updated! Please click the button below to reload the page.",
"components.StatusChacker.newversionavailable": "Application Update", "components.StatusChacker.newversionavailable": "Application Update",

View File

@@ -1,7 +1,6 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@tailwind screens;
html { html {
min-height: calc(100% + env(safe-area-inset-top)); min-height: calc(100% + env(safe-area-inset-top));
@@ -182,7 +181,7 @@ a.crew-name,
} }
.media-ratings { .media-ratings {
@apply flex items-center justify-center px-4 py-2 border-b border-gray-700 last:border-b-0; @apply flex items-center justify-center px-4 py-2 font-medium border-b border-gray-700 last:border-b-0;
} }
.media-rating { .media-rating {
@@ -213,6 +212,10 @@ img.avatar-sm {
@apply mr-2 font-bold; @apply mr-2 font-bold;
} }
.card-field a {
@apply transition duration-300 hover:text-white hover:underline;
}
.section { .section {
@apply mt-6 mb-10 text-white; @apply mt-6 mb-10 text-white;
} }