Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/interfaces/api/settingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface PublicSettingsResponse {
mediaServerType: number;
partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean;
separate4kRequestButton: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
Expand Down
4 changes: 4 additions & 0 deletions server/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface MainSettings {
mediaServerType: number;
partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean;
separate4kRequestButton: boolean;
locale: string;
youtubeUrl: string;
}
Expand Down Expand Up @@ -206,6 +207,7 @@ interface FullPublicSettings extends PublicSettings {
jellyfinServerName?: string;
partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean;
separate4kRequestButton: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
Expand Down Expand Up @@ -426,6 +428,7 @@ class Settings {
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,
enableSpecialEpisodes: false,
separate4kRequestButton: false,
locale: 'en',
youtubeUrl: '',
},
Expand Down Expand Up @@ -724,6 +727,7 @@ class Settings {
mediaServerType: this.main.mediaServerType,
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
enableSpecialEpisodes: this.data.main.enableSpecialEpisodes,
separate4kRequestButton: this.data.main.separate4kRequestButton,
cacheImages: this.data.main.cacheImages,
vapidPublic: this.vapidPublic,
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
Expand Down
1 change: 1 addition & 0 deletions src/components/CollectionDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
status4k={title.mediaInfo?.status4k}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
Expand Down
12 changes: 10 additions & 2 deletions src/components/Common/Badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@ interface BadgeProps {
| 'success'
| 'dark'
| 'light';
size?: 'default' | 'lg';
className?: string;
href?: string;
children: React.ReactNode;
}

const Badge = (
{ badgeType = 'default', className, href, children }: BadgeProps,
{
badgeType = 'default',
size = 'default',
className,
href,
children,
}: BadgeProps,
ref?: React.Ref<HTMLElement>
) => {
const badgeStyle = [
'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap',
'inline-flex items-center font-semibold rounded-full whitespace-nowrap',
size === 'lg' ? 'px-3 py-1 text-sm leading-5' : 'px-2 text-xs leading-5',
];

if (href) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Common/ListView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const ListView = ({
}
image={title.posterPath}
status={title.mediaInfo?.status}
status4k={title.mediaInfo?.status4k}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
Expand All @@ -109,6 +110,7 @@ const ListView = ({
}
image={title.posterPath}
status={title.mediaInfo?.status}
status4k={title.mediaInfo?.status4k}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
Expand Down
10 changes: 7 additions & 3 deletions src/components/Common/StatusBadgeMini/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const StatusBadgeMini = ({
}: StatusBadgeMiniProps) => {
const badgeStyle = [
`rounded-full shadow-md ${
shrink ? 'w-4 sm:w-5 border p-0' : 'w-5 ring-1 p-0.5'
shrink ? 'h-4 w-4 border p-0 sm:h-5 sm:w-5' : 'h-5 w-5 ring-1 p-0.5'
}`,
];

Expand Down Expand Up @@ -72,12 +72,16 @@ const StatusBadgeMini = ({

return (
<div
className={`relative inline-flex whitespace-nowrap rounded-full border-gray-700 text-xs font-semibold leading-5 ring-gray-700 ${
className={`relative inline-flex items-center whitespace-nowrap rounded-full border-gray-700 text-xs font-semibold leading-5 ring-gray-700 ${
shrink ? '' : 'ring-1'
}`}
>
{is4k && (
<span className="pl-2 pr-1 text-white [text-shadow:-0.5px_-0.5px_0_#000,0.5px_-0.5px_0_#000,-0.5px_0.5px_0_#000,0.5px_0.5px_0_#000]">
4K
</span>
)}
<div className={badgeStyle.join(' ')}>{indicatorIcon}</div>
{is4k && <span className="pl-1 pr-2 text-gray-200">4K</span>}
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/components/MediaSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const MediaSlider = ({
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
status4k={title.mediaInfo?.status4k}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
Expand All @@ -143,6 +144,7 @@ const MediaSlider = ({
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
status4k={title.mediaInfo?.status4k}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
Expand Down
2 changes: 2 additions & 0 deletions src/components/MovieDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
mediaType="movie"
plexUrl={plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl}
size="lg"
/>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
Expand All @@ -537,6 +538,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
mediaType="movie"
plexUrl={plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
size="lg"
/>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/components/PersonDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ const PersonDetails = () => {
summary={media.overview}
mediaType={media.mediaType as 'movie' | 'tv'}
status={media.mediaInfo?.status}
status4k={media.mediaInfo?.status4k}
canExpand
/>
{media.character && (
Expand Down Expand Up @@ -224,6 +225,7 @@ const PersonDetails = () => {
summary={media.overview}
mediaType={media.mediaType as 'movie' | 'tv'}
status={media.mediaInfo?.status}
status4k={media.mediaInfo?.status4k}
canExpand
/>
{media.job && (
Expand Down
70 changes: 46 additions & 24 deletions src/components/RequestButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,54 @@ const RequestButton = ({
});
}

const [buttonOne, ...others] = buttons;
// When enabled, split all 4K-related request actions into their own dedicated
// button (with its own dropdown), leaving the non-4K actions untouched.
// Otherwise everything stays in a single button + dropdown.
const splitButtons = settings.currentSettings.separate4kRequestButton;
const mainButtons = splitButtons
? buttons.filter((button) => !button.id.includes('4k'))
: buttons;
const fourKButtons = splitButtons
? buttons.filter((button) => button.id.includes('4k'))
: [];

if (!buttonOne) {
if (mainButtons.length === 0 && fourKButtons.length === 0) {
return null;
}

const renderButtonGroup = (group: ButtonOption[]) => {
const [buttonOne, ...others] = group;

if (!buttonOne) {
return null;
}

return (
<ButtonWithDropdown
text={
<>
{buttonOne.svg}
<span>{buttonOne.text}</span>
</>
}
onClick={buttonOne.action}
className="ml-2"
>
{others.length > 0
? others.map((button) => (
<ButtonWithDropdown.Item
onClick={button.action}
key={`request-option-${button.id}`}
>
{button.svg}
<span>{button.text}</span>
</ButtonWithDropdown.Item>
))
: null}
</ButtonWithDropdown>
);
};

return (
<>
<RequestModal
Expand All @@ -391,28 +433,8 @@ const RequestButton = ({
}}
onCancel={() => setShowRequest4kModal(false)}
/>
<ButtonWithDropdown
text={
<>
{buttonOne.svg}
<span>{buttonOne.text}</span>
</>
}
onClick={buttonOne.action}
className="ml-2"
>
{others && others.length > 0
? others.map((button) => (
<ButtonWithDropdown.Item
onClick={button.action}
key={`request-option-${button.id}`}
>
{button.svg}
<span>{button.text}</span>
</ButtonWithDropdown.Item>
))
: null}
</ButtonWithDropdown>
{renderButtonGroup(mainButtons)}
{renderButtonGroup(fourKButtons)}
</>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/RequestModal/TvRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const TvRequestModal = ({

if (response.data) {
if (onComplete) {
onComplete(response.data.media.status);
onComplete(response.data.media[is4k ? 'status4k' : 'status']);
}
addToast(
<span>
Expand Down
31 changes: 31 additions & 0 deletions src/components/Settings/SettingsMain/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const messages = defineMessages('components.Settings.SettingsMain', {
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
partialRequestsEnabled: 'Allow Partial Series Requests',
enableSpecialEpisodes: 'Allow Special Episodes Requests',
separate4kRequestButton: 'Separate 4K Request Button',
separate4kRequestButtonTip:
'Show the 4K request as its own button next to the Request button on title pages instead of inside the request dropdown',
locale: 'Display Language',
youtubeUrl: 'YouTube URL',
youtubeUrlTip:
Expand Down Expand Up @@ -181,6 +184,7 @@ const SettingsMain = () => {
blocklistedTagsLimit: data?.blocklistedTagsLimit || 50,
partialRequestsEnabled: data?.partialRequestsEnabled,
enableSpecialEpisodes: data?.enableSpecialEpisodes,
separate4kRequestButton: data?.separate4kRequestButton,
cacheImages: data?.cacheImages,
youtubeUrl: data?.youtubeUrl,
}}
Expand All @@ -203,6 +207,7 @@ const SettingsMain = () => {
blocklistedTagsLimit: values.blocklistedTagsLimit,
partialRequestsEnabled: values.partialRequestsEnabled,
enableSpecialEpisodes: values.enableSpecialEpisodes,
separate4kRequestButton: values.separate4kRequestButton,
cacheImages: values.cacheImages,
youtubeUrl: values.youtubeUrl,
});
Expand Down Expand Up @@ -584,6 +589,32 @@ const SettingsMain = () => {
/>
</div>
</div>
<div className="form-row">
<label
htmlFor="separate4kRequestButton"
className="checkbox-label"
>
<span className="mr-2">
{intl.formatMessage(messages.separate4kRequestButton)}
</span>
<span className="label-tip">
{intl.formatMessage(messages.separate4kRequestButtonTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="separate4kRequestButton"
name="separate4kRequestButton"
onChange={() => {
setFieldValue(
'separate4kRequestButton',
!values.separate4kRequestButton
);
}}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="youtubeUrl" className="text-label">
{intl.formatMessage(messages.youtubeUrl)}
Expand Down
Loading
Loading