From cb5520a08b059516143866b97f59aa85a2e8240d Mon Sep 17 00:00:00 2001 From: Flawkee <70835293+Flawkee@users.noreply.github.com> Date: Thu, 21 May 2026 17:03:24 +0300 Subject: [PATCH 1/6] feat(title cards + page): improve 4K content visibility and related user experience improve 4K content visibility and enable inline request from thumbnails + status and availability + separate 4k request button in title page admin config "re #2598", "re #1826" --- next-env.d.ts | 2 +- server/interfaces/api/settingsInterfaces.ts | 1 + server/lib/settings/index.ts | 4 + src/components/CollectionDetails/index.tsx | 1 + src/components/Common/ListView/index.tsx | 2 + .../Common/StatusBadgeMini/index.tsx | 10 +- src/components/MediaSlider/index.tsx | 2 + src/components/PersonDetails/index.tsx | 2 + src/components/RequestButton/index.tsx | 70 +-- .../RequestModal/TvRequestModal.tsx | 2 +- .../Settings/SettingsMain/index.tsx | 31 ++ src/components/TitleCard/TmdbTitleCard.tsx | 2 + src/components/TitleCard/index.tsx | 402 ++++++++++++------ src/context/SettingsContext.tsx | 1 + src/i18n/locale/en.json | 5 + src/pages/_app.tsx | 1 + 16 files changed, 390 insertions(+), 148 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 19709046af..7996d352f4 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 3dfa619205..51b99d47bf 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -41,6 +41,7 @@ export interface PublicSettingsResponse { mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + separate4kRequestButton: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index c1124c22f4..1eb2e6c07a 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -154,6 +154,7 @@ export interface MainSettings { mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + separate4kRequestButton: boolean; locale: string; youtubeUrl: string; } @@ -206,6 +207,7 @@ interface FullPublicSettings extends PublicSettings { jellyfinServerName?: string; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + separate4kRequestButton: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -426,6 +428,7 @@ class Settings { mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, + separate4kRequestButton: false, locale: 'en', youtubeUrl: '', }, @@ -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, diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index a8ee90a684..e51670a78f 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -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} diff --git a/src/components/Common/ListView/index.tsx b/src/components/Common/ListView/index.tsx index 142d8f77e2..6b7440386f 100644 --- a/src/components/Common/ListView/index.tsx +++ b/src/components/Common/ListView/index.tsx @@ -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} @@ -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} diff --git a/src/components/Common/StatusBadgeMini/index.tsx b/src/components/Common/StatusBadgeMini/index.tsx index ea93a2a9b5..262ee90113 100644 --- a/src/components/Common/StatusBadgeMini/index.tsx +++ b/src/components/Common/StatusBadgeMini/index.tsx @@ -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' }`, ]; @@ -72,12 +72,16 @@ const StatusBadgeMini = ({ return (
+ {is4k && ( + + 4K + + )}
{indicatorIcon}
- {is4k && 4K}
); }; diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index 4418c12314..a2e2aeee2a 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -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} @@ -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} diff --git a/src/components/PersonDetails/index.tsx b/src/components/PersonDetails/index.tsx index c8538793c2..38fca5ed81 100644 --- a/src/components/PersonDetails/index.tsx +++ b/src/components/PersonDetails/index.tsx @@ -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 && ( @@ -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 && ( diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index 55b14c6c96..8683b012f7 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -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 ( + + {buttonOne.svg} + {buttonOne.text} + + } + onClick={buttonOne.action} + className="ml-2" + > + {others.length > 0 + ? others.map((button) => ( + + {button.svg} + {button.text} + + )) + : null} + + ); + }; + return ( <> setShowRequest4kModal(false)} /> - - {buttonOne.svg} - {buttonOne.text} - - } - onClick={buttonOne.action} - className="ml-2" - > - {others && others.length > 0 - ? others.map((button) => ( - - {button.svg} - {button.text} - - )) - : null} - + {renderButtonGroup(mainButtons)} + {renderButtonGroup(fourKButtons)} ); }; diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 941b8dc71e..cbab91c3d8 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -210,7 +210,7 @@ const TvRequestModal = ({ if (response.data) { if (onComplete) { - onComplete(response.data.media.status); + onComplete(response.data.media[is4k ? 'status4k' : 'status']); } addToast( diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 00703ddd37..44878e8427 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -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: @@ -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, }} @@ -203,6 +207,7 @@ const SettingsMain = () => { blocklistedTagsLimit: values.blocklistedTagsLimit, partialRequestsEnabled: values.partialRequestsEnabled, enableSpecialEpisodes: values.enableSpecialEpisodes, + separate4kRequestButton: values.separate4kRequestButton, cacheImages: values.cacheImages, youtubeUrl: values.youtubeUrl, }); @@ -584,6 +589,32 @@ const SettingsMain = () => { /> +
+ +
+ { + setFieldValue( + 'separate4kRequestButton', + !values.separate4kRequestButton + ); + }} + /> +
+
+
+ +
+ { + setFieldValue( + 'multiQualityRequestsEnabled', + !values.multiQualityRequestsEnabled + ); + }} + /> +
+