From 8a23110dd60c71dfe43358ec31e0db1c929f5a95 Mon Sep 17 00:00:00 2001 From: supatuck Date: Sun, 16 Aug 2026 13:15:52 -0600 Subject: [PATCH] feat(ui): add option to display 4K availability on title cards Adds an opt-in main setting, merge4kAvailability, off by default. When enabled, a title whose standard version is unknown or deleted but whose 4K version is available or partially available shows the availability check on discover cards, with the 4K tag on the badge. The merge only applies for users who could see the 4K status anyway: it requires 4K to be enabled for that media type and the user to hold a matching 4K request permission or Manage Requests. Detail pages already show a separate 4K badge and are unchanged. Users who keep the setting off see no behavior change. Motivation: on libraries where HD copies are routinely replaced by 4K upgrades, cards for owned titles show no availability at a glance. Related discussions: #367, #1744 --- seerr-api.yml | 3 ++ 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 + src/components/MediaSlider/index.tsx | 2 + src/components/PersonDetails/index.tsx | 2 + .../Settings/SettingsMain/index.tsx | 31 ++++++++++++++ src/components/TitleCard/TmdbTitleCard.tsx | 2 + src/components/TitleCard/index.tsx | 42 +++++++++++++++++-- src/context/SettingsContext.tsx | 1 + src/i18n/locale/en.json | 2 + src/pages/_app.tsx | 1 + 13 files changed, 91 insertions(+), 3 deletions(-) diff --git a/seerr-api.yml b/seerr-api.yml index f691df2905..380ff8c984 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -232,6 +232,9 @@ components: hideAvailable: type: boolean example: false + merge4kAvailability: + type: boolean + example: false partialRequestsEnabled: type: boolean example: false diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index f2793c9aad..f4c2d1cb20 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -31,6 +31,7 @@ export interface PublicSettingsResponse { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + merge4kAvailability: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 0a896524eb..0e2f18531d 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -141,6 +141,7 @@ export interface MainSettings { }; hideAvailable: boolean; hideBlocklisted: boolean; + merge4kAvailability: boolean; localLogin: boolean; mediaServerLogin: boolean; newPlexLogin: boolean; @@ -194,6 +195,7 @@ interface FullPublicSettings extends PublicSettings { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + merge4kAvailability: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; @@ -416,6 +418,7 @@ class Settings { }, hideAvailable: false, hideBlocklisted: false, + merge4kAvailability: false, localLogin: true, mediaServerLogin: true, newPlexLogin: true, @@ -712,6 +715,7 @@ class Settings { applicationUrl: this.data.main.applicationUrl, hideAvailable: this.data.main.hideAvailable, hideBlocklisted: this.data.main.hideBlocklisted, + merge4kAvailability: this.data.main.merge4kAvailability, localLogin: this.data.main.localLogin, mediaServerLogin: this.data.main.mediaServerLogin, jellyfinExternalHost: this.data.jellyfin.externalHostname, 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/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/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 259a297a71..3c6b39265d 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -62,6 +62,9 @@ const messages = defineMessages('components.Settings.SettingsMain', { hideAvailable: 'Hide Available Media', hideAvailableTip: 'Hide available media from the discover pages but not search results', + merge4kAvailability: 'Display 4K Availability on Title Cards', + merge4kAvailabilityTip: + 'Show titles that are only available in 4K as available on discover cards', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', @@ -173,6 +176,7 @@ const SettingsMain = () => { applicationUrl: data?.applicationUrl, hideAvailable: data?.hideAvailable, hideBlocklisted: data?.hideBlocklisted, + merge4kAvailability: data?.merge4kAvailability, locale: data?.locale ?? 'en', discoverRegion: data?.discoverRegion, originalLanguage: data?.originalLanguage, @@ -196,6 +200,7 @@ const SettingsMain = () => { applicationUrl: values.applicationUrl, hideAvailable: values.hideAvailable, hideBlocklisted: values.hideBlocklisted, + merge4kAvailability: values.merge4kAvailability, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -519,6 +524,32 @@ const SettingsMain = () => { /> +
+ +
+ { + setFieldValue( + 'merge4kAvailability', + !values.merge4kAvailability + ); + }} + /> +
+