Nix configurations for my homelab
0

Configure Feed

Select the types of activity you want to include in your feed.

Add patches for Jellyfin to blur information for unwatched episodes

+1276
+16
modules/services/jellyfin.nix
··· 2 2 { 3 3 imports = [ ../unfree.nix ]; 4 4 5 + nixpkgs.overlays = [ 6 + (final: prev: { 7 + jellyfin-web = prev.jellyfin-web.overrideAttrs ( 8 + finalAttrs: prevAttrs: { 9 + patches = (prevAttrs.patches or [ ]) ++ [ 10 + ../../patches/jellyfin-web/0001-feat-Add-blur-title-and-description-settings.patch 11 + ../../patches/jellyfin-web/0002-remove-blur-audio-title.patch 12 + ../../patches/jellyfin-web/0003-refactor-getListViewHtml-to-receive-new-settings-in-.patch 13 + ../../patches/jellyfin-web/0004-blur-title-and-description-in-episode-details-and-Mo.patch 14 + ../../patches/jellyfin-web/0005-fix-lint.patch 15 + ]; 16 + } 17 + ); 18 + }) 19 + ]; 20 + 5 21 environment.persistence."/data/persistent".directories = [ 6 22 { 7 23 directory = "/var/cache/jellyfin";
+878
patches/jellyfin-web/0001-feat-Add-blur-title-and-description-settings.patch
··· 1 + From c1111acebb4792e69f7b2bf7152957c03058d4cd Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Natal=C3=AD=20Paura?= 3 + <30585029+natilou@users.noreply.github.com> 4 + Date: Sat, 3 Jan 2026 20:54:10 -0300 5 + Subject: [PATCH 1/5] feat: Add blur title and description settings 6 + 7 + - Add and implement two new settings to enable blur title and description for unplayed episodes. 8 + These settings allows to hide episode title and description until played or hovered. 9 + --- 10 + .../components/DisplayPreferences.tsx | 34 +++++++++++++++++++ 11 + .../preferences/hooks/useDisplaySettings.ts | 4 +++ 12 + .../types/displaySettingsValues.ts | 2 ++ 13 + src/components/cardbuilder/card.scss | 9 +++++ 14 + src/components/cardbuilder/cardBuilder.js | 15 ++++++-- 15 + .../displaySettings/displaySettings.js | 4 +++ 16 + .../displaySettings.template.html | 16 +++++++++ 17 + .../homesections/sections/nextUp.ts | 4 ++- 18 + src/components/listview/List/List.tsx | 4 +++ 19 + src/components/listview/List/ListContent.tsx | 4 +++ 20 + src/components/listview/List/ListItemBody.tsx | 9 ++++- 21 + src/components/listview/List/ListWrapper.tsx | 11 ++++-- 22 + src/components/listview/List/useList.ts | 6 +++- 23 + src/components/listview/listview.js | 20 ++++++++--- 24 + src/components/listview/listview.scss | 9 +++++ 25 + src/components/remotecontrol/remotecontrol.js | 5 ++- 26 + src/controllers/itemDetails/index.js | 14 ++++---- 27 + src/controllers/list.js | 10 ++++-- 28 + src/controllers/shows/episodes.js | 3 +- 29 + src/controllers/shows/tvshows.js | 3 +- 30 + src/hooks/useUserSettings.tsx | 19 +++++++++-- 31 + src/scripts/itemsByName.js | 3 +- 32 + src/scripts/playlistViewer.js | 3 +- 33 + src/scripts/settings/userSettings.js | 28 +++++++++++++++ 34 + src/strings/en-gb.json | 4 +++ 35 + src/strings/en-us.json | 4 +++ 36 + src/strings/es-ar.json | 4 +++ 37 + src/strings/es-mx.json | 4 +++ 38 + src/strings/es.json | 4 +++ 39 + src/types/listOptions.ts | 2 ++ 40 + 30 files changed, 231 insertions(+), 30 deletions(-) 41 + 42 + diff --git a/src/apps/experimental/features/preferences/components/DisplayPreferences.tsx b/src/apps/experimental/features/preferences/components/DisplayPreferences.tsx 43 + index e5e940b7aa..6b0af542d2 100644 44 + --- a/src/apps/experimental/features/preferences/components/DisplayPreferences.tsx 45 + +++ b/src/apps/experimental/features/preferences/components/DisplayPreferences.tsx 46 + @@ -202,6 +202,40 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere 47 + {globalize.translate('EnableBlurHashHelp')} 48 + </FormHelperText> 49 + </FormControl> 50 + + 51 + + <FormControl fullWidth> 52 + + <FormControlLabel 53 + + aria-describedby='display-settings-blurUnplayed-title' 54 + + control={ 55 + + <Checkbox 56 + + checked={values.enableBlurUnplayedTitle} 57 + + onChange={onChange} 58 + + /> 59 + + } 60 + + label={globalize.translate('EnableBlurUnplayedTitle')} 61 + + name='enableBlurUnplayedTitle' 62 + + /> 63 + + <FormHelperText id='display-settings-unplayed-title'> 64 + + {globalize.translate('EnableBlurUnplayedTitleHelp')} 65 + + </FormHelperText> 66 + + </FormControl> 67 + + 68 + + <FormControl fullWidth> 69 + + <FormControlLabel 70 + + aria-describedby='display-settings-blurUnplayed-description' 71 + + control={ 72 + + <Checkbox 73 + + checked={values.enableBlurUnplayedDescription} 74 + + onChange={onChange} 75 + + /> 76 + + } 77 + + label={globalize.translate('EnableBlurUnplayedDescription')} 78 + + name='enableBlurUnplayedDescription' 79 + + /> 80 + + <FormHelperText id='display-settings-unplayed-description'> 81 + + {globalize.translate('EnableBlurUnplayedDescriptionHelp')} 82 + + </FormHelperText> 83 + + </FormControl> 84 + </Stack> 85 + ); 86 + } 87 + diff --git a/src/apps/experimental/features/preferences/hooks/useDisplaySettings.ts b/src/apps/experimental/features/preferences/hooks/useDisplaySettings.ts 88 + index ee44a80ace..97e220171a 100644 89 + --- a/src/apps/experimental/features/preferences/hooks/useDisplaySettings.ts 90 + +++ b/src/apps/experimental/features/preferences/hooks/useDisplaySettings.ts 91 + @@ -90,6 +90,8 @@ async function loadDisplaySettings({ 92 + disableCustomCss: Boolean(settings.disableCustomCss()), 93 + displayMissingEpisodes: user?.Configuration?.DisplayMissingEpisodes ?? false, 94 + enableBlurHash: Boolean(settings.enableBlurhash()), 95 + + enableBlurUnplayedTitle: Boolean(settings.enableBlurUnplayedTitle()), 96 + + enableBlurUnplayedDescription: Boolean(settings.enableBlurUnplayedDescription()), 97 + enableFasterAnimation: Boolean(settings.enableFastFadein()), 98 + enableItemDetailsBanner: Boolean(settings.detailsBanner()), 99 + enableLibraryBackdrops: Boolean(settings.enableBackdrops()), 100 + @@ -135,6 +137,8 @@ async function saveDisplaySettings({ 101 + userSettings.dateTimeLocale(normalizeValue(newDisplaySettings.dateTimeLocale)); 102 + userSettings.disableCustomCss(newDisplaySettings.disableCustomCss); 103 + userSettings.enableBlurhash(newDisplaySettings.enableBlurHash); 104 + + userSettings.enableBlurUnplayedTitle(newDisplaySettings.enableBlurUnplayedTitle); 105 + + userSettings.enableBlurUnplayedDescription(newDisplaySettings.enableBlurUnplayedDescription); 106 + userSettings.enableFastFadein(newDisplaySettings.enableFasterAnimation); 107 + userSettings.detailsBanner(newDisplaySettings.enableItemDetailsBanner); 108 + userSettings.enableBackdrops(newDisplaySettings.enableLibraryBackdrops); 109 + diff --git a/src/apps/experimental/features/preferences/types/displaySettingsValues.ts b/src/apps/experimental/features/preferences/types/displaySettingsValues.ts 110 + index a5e08d2dd9..de1a8a79f3 100644 111 + --- a/src/apps/experimental/features/preferences/types/displaySettingsValues.ts 112 + +++ b/src/apps/experimental/features/preferences/types/displaySettingsValues.ts 113 + @@ -5,6 +5,8 @@ export interface DisplaySettingsValues { 114 + disableCustomCss: boolean; 115 + displayMissingEpisodes: boolean; 116 + enableBlurHash: boolean; 117 + + enableBlurUnplayedTitle: boolean; 118 + + enableBlurUnplayedDescription: boolean; 119 + enableFasterAnimation: boolean; 120 + enableItemDetailsBanner: boolean; 121 + enableLibraryBackdrops: boolean; 122 + diff --git a/src/components/cardbuilder/card.scss b/src/components/cardbuilder/card.scss 123 + index 737850261b..7391938cdc 100644 124 + --- a/src/components/cardbuilder/card.scss 125 + +++ b/src/components/cardbuilder/card.scss 126 + @@ -379,6 +379,15 @@ button::-moz-focus-inner { 127 + margin-right: 2em; 128 + } 129 + 130 + +.cardText-blurred { 131 + + filter: blur(5px); 132 + + transition: filter 0.3s ease; 133 + +} 134 + + 135 + +.card:hover .cardText-blurred { 136 + + filter: blur(0); 137 + +} 138 + + 139 + .cardImageContainer > .cardDefaultText { 140 + white-space: normal; 141 + text-align: center; 142 + diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js 143 + index a955f992dc..a3795ed180 100644 144 + --- a/src/components/cardbuilder/cardBuilder.js 145 + +++ b/src/components/cardbuilder/cardBuilder.js 146 + @@ -562,14 +562,19 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, 147 + includeParentInfo: options.includeParentInfoInTitle 148 + }); 149 + 150 + - lines.push(getTextActionButton({ 151 + + let titleHtml = getTextActionButton({ 152 + Id: item.Id, 153 + ServerId: serverId, 154 + Name: name, 155 + Type: item.Type, 156 + CollectionType: item.CollectionType, 157 + IsFolder: item.IsFolder 158 + - })); 159 + + }); 160 + + 161 + + if (options.enableBlurUnplayedTitle && item.UserData && !item.UserData.Played) { 162 + + titleHtml = `<span class="cardText-blurred">${titleHtml}</span>`; 163 + + } 164 + + lines.push(titleHtml); 165 + } 166 + 167 + if (showOtherText) { 168 + @@ -737,7 +742,11 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, 169 + } 170 + 171 + if (flags.overlayText && showTitle) { 172 + - lines = [escapeHtml(item.Name)]; 173 + + let overlayTitle = escapeHtml(item.Name); 174 + + if (options.enableBlurUnplayedTitle && item.UserData && !item.UserData.Played) { 175 + + overlayTitle = `<span class="cardText-blurred">${overlayTitle}</span>`; 176 + + } 177 + + lines = [overlayTitle]; 178 + } 179 + 180 + const addRightTextMargin = flags.isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile; 181 + diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js 182 + index 7ca0e11b6d..032f04eb7f 100644 183 + --- a/src/components/displaySettings/displaySettings.js 184 + +++ b/src/components/displaySettings/displaySettings.js 185 + @@ -120,6 +120,8 @@ function loadForm(context, user, userSettings) { 186 + context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos(); 187 + context.querySelector('#chkFadein').checked = userSettings.enableFastFadein(); 188 + context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash(); 189 + + context.querySelector('#chkBlurUnplayedTitle').checked = userSettings.enableBlurUnplayedTitle(); 190 + + context.querySelector('chkBlurUnplayedDescription').checked = userSettings.enableBlurUnplayedDescription(); 191 + context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops(); 192 + context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner(); 193 + 194 + @@ -167,6 +169,8 @@ function saveUser(context, user, userSettingsInstance, apiClient) { 195 + 196 + userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked); 197 + userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked); 198 + + userSettingsInstance.enableBlurUnplayedTitle(context.querySelector('#chkBlurUnplayedTitle').checked); 199 + + userSettingsInstance.enableBlurUnplayedDescription(context.querySelector('#chkBlurUnplayedDescription').checked); 200 + userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked); 201 + userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked); 202 + 203 + diff --git a/src/components/displaySettings/displaySettings.template.html b/src/components/displaySettings/displaySettings.template.html 204 + index 1b5579b4c3..bc483b83c6 100644 205 + --- a/src/components/displaySettings/displaySettings.template.html 206 + +++ b/src/components/displaySettings/displaySettings.template.html 207 + @@ -230,6 +230,22 @@ <h2 class="sectionTitle"> 208 + <div class="fieldDescription checkboxFieldDescription">${EnableBlurHashHelp}</div> 209 + </div> 210 + 211 + + <div class="checkboxContainer checkboxContainer-withDescription"> 212 + + <label> 213 + + <input type="checkbox" is="emby-checkbox" id="chkBlurUnplayedTitle" /> 214 + + <span>${enableBlurUnplayedTitle}</span> 215 + + </label> 216 + + <div class="fieldDescription checkboxFieldDescription">${EnableBlurUnplayedTitleHelp}</div> 217 + + </div> 218 + + 219 + + <div class="checkboxContainer checkboxContainer-withDescription"> 220 + + <label> 221 + + <input type="checkbox" is="emby-checkbox" id="chkBlurUnplayedDescription" /> 222 + + <span>${enableBlurUnplayedDescription}</span> 223 + + </label> 224 + + <div class="fieldDescription checkboxFieldDescription">${EnableBlurUnplayedDescriptionHelp}</div> 225 + + </div> 226 + + 227 + <h2 class="sectionTitle"> 228 + ${HeaderLibraries} 229 + </h2> 230 + diff --git a/src/components/homesections/sections/nextUp.ts b/src/components/homesections/sections/nextUp.ts 231 + index 539472bbf0..4ace6066e7 100644 232 + --- a/src/components/homesections/sections/nextUp.ts 233 + +++ b/src/components/homesections/sections/nextUp.ts 234 + @@ -37,6 +37,7 @@ function getNextUpFetchFn( 235 + 236 + function getNextUpItemsHtmlFn( 237 + useEpisodeImages: boolean, 238 + + enableBlurUnplayedTitle: boolean, 239 + { enableOverflow }: SectionOptions 240 + ) { 241 + return function (items: BaseItemDto[]) { 242 + @@ -49,6 +50,7 @@ function getNextUpItemsHtmlFn( 243 + overlayText: false, 244 + showTitle: true, 245 + showParentTitle: true, 246 + + enableBlurUnplayedTitle, 247 + lazy: true, 248 + overlayPlayButton: true, 249 + context: 'home', 250 + @@ -102,6 +104,6 @@ export function loadNextUp( 251 + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); 252 + if (!itemsContainer) return; 253 + itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options); 254 + - itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options); 255 + + itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), userSettings.enableBlurUnplayedTitle(), options); 256 + itemsContainer.parentContainer = elem; 257 + } 258 + diff --git a/src/components/listview/List/List.tsx b/src/components/listview/List/List.tsx 259 + index 8afe3503ba..3b46496e13 100644 260 + --- a/src/components/listview/List/List.tsx 261 + +++ b/src/components/listview/List/List.tsx 262 + @@ -18,11 +18,15 @@ const List: FC<ListProps> = ({ index, item, listOptions = {} }) => { 263 + const listWrapperProps = getListdWrapperProps(); 264 + const listContentProps = getListContentProps(); 265 + 266 + + const isPlayed = Boolean(item.UserData?.Played); 267 + + const blurUnplayedTitle = Boolean(listWrapperProps.enableBlurUnplayedTitle) && !isPlayed; 268 + + 269 + return ( 270 + <ListWrapper 271 + key={index} 272 + index={index} 273 + {...listWrapperProps} 274 + + enableBlurUnplayedTitle={blurUnplayedTitle} 275 + > 276 + <ListContent {...listContentProps} /> 277 + </ListWrapper> 278 + diff --git a/src/components/listview/List/ListContent.tsx b/src/components/listview/List/ListContent.tsx 279 + index 0b9bc03d01..26c7e0e98e 100644 280 + --- a/src/components/listview/List/ListContent.tsx 281 + +++ b/src/components/listview/List/ListContent.tsx 282 + @@ -37,6 +37,9 @@ const ListContent: FC<ListContentProps> = ({ 283 + downloadWidth 284 + }) => { 285 + const indicator = useIndicator(item); 286 + + const isPlayed = Boolean(item.UserData?.Played); 287 + + const blurUnplayedDescription = Boolean(listOptions.enableBlurUnplayedDescription) && !isPlayed; 288 + + 289 + return ( 290 + <ListContentWrapper 291 + itemOverview={item.Overview} 292 + @@ -71,6 +74,7 @@ const ListContent: FC<ListContentProps> = ({ 293 + action={action} 294 + enableContentWrapper={enableContentWrapper} 295 + enableOverview={enableOverview} 296 + + enableBlurUnplayedDescription={blurUnplayedDescription} 297 + enableSideMediaInfo={enableSideMediaInfo} 298 + getMissingIndicator={indicator.getMissingIndicator} 299 + /> 300 + diff --git a/src/components/listview/List/ListItemBody.tsx b/src/components/listview/List/ListItemBody.tsx 301 + index 90e9cb502e..23760618f7 100644 302 + --- a/src/components/listview/List/ListItemBody.tsx 303 + +++ b/src/components/listview/List/ListItemBody.tsx 304 + @@ -16,6 +16,7 @@ interface ListItemBodyProps { 305 + clickEntireItem?: boolean; 306 + enableContentWrapper?: boolean; 307 + enableOverview?: boolean; 308 + + enableBlurUnplayedDescription?: boolean; 309 + enableSideMediaInfo?: boolean; 310 + getMissingIndicator: () => React.JSX.Element | null 311 + } 312 + @@ -28,6 +29,7 @@ const ListItemBody: FC<ListItemBodyProps> = ({ 313 + clickEntireItem, 314 + enableContentWrapper, 315 + enableOverview, 316 + + enableBlurUnplayedDescription, 317 + enableSideMediaInfo, 318 + getMissingIndicator 319 + }) => { 320 + @@ -71,7 +73,12 @@ const ListItemBody: FC<ListItemBodyProps> = ({ 321 + )} 322 + 323 + {!enableContentWrapper && enableOverview && item.Overview && ( 324 + - <Box className='secondary listItem-overview listItemBodyText'> 325 + + <Box 326 + + className={classNames( 327 + + 'secondary listItem-overview listItemBodyText', 328 + + enableBlurUnplayedDescription && 'listItemBodyText-blurred' 329 + + )} 330 + + > 331 + <bdi>{item.Overview}</bdi> 332 + </Box> 333 + )} 334 + diff --git a/src/components/listview/List/ListWrapper.tsx b/src/components/listview/List/ListWrapper.tsx 335 + index 800706409f..cd246cb9f5 100644 336 + --- a/src/components/listview/List/ListWrapper.tsx 337 + +++ b/src/components/listview/List/ListWrapper.tsx 338 + @@ -11,6 +11,7 @@ interface ListWrapperProps { 339 + action?: string | null; 340 + dataAttributes?: DataAttributes; 341 + className?: string; 342 + + enableBlurUnplayedTitle?: boolean; 343 + } 344 + 345 + const ListWrapper: FC<PropsWithChildren<ListWrapperProps>> = ({ 346 + @@ -19,14 +20,20 @@ const ListWrapper: FC<PropsWithChildren<ListWrapperProps>> = ({ 347 + title, 348 + className, 349 + dataAttributes, 350 + + enableBlurUnplayedTitle, 351 + children 352 + }) => { 353 + + const wrapperClassName = classNames( 354 + + className, 355 + + enableBlurUnplayedTitle && 'listItemBodyText-blurred' 356 + + ); 357 + + 358 + if (layoutManager.tv) { 359 + return ( 360 + <Button 361 + data-index={index} 362 + className={classNames( 363 + - className, 364 + + wrapperClassName, 365 + 'itemAction listItem-button listItem-focusscale' 366 + )} 367 + data-action={action} 368 + @@ -38,7 +45,7 @@ const ListWrapper: FC<PropsWithChildren<ListWrapperProps>> = ({ 369 + ); 370 + } else { 371 + return ( 372 + - <Box data-index={index} className={className} {...dataAttributes}> 373 + + <Box data-index={index} className={wrapperClassName} {...dataAttributes}> 374 + {children} 375 + </Box> 376 + ); 377 + diff --git a/src/components/listview/List/useList.ts b/src/components/listview/List/useList.ts 378 + index 196721a0dc..9c1a38bd28 100644 379 + --- a/src/components/listview/List/useList.ts 380 + +++ b/src/components/listview/List/useList.ts 381 + @@ -2,6 +2,7 @@ import classNames from 'classnames'; 382 + import { getDataAttributes } from 'utils/items'; 383 + import layoutManager from 'components/layoutManager'; 384 + 385 + +import { useUserSettings } from 'hooks/useUserSettings'; 386 + import type { ItemDto } from 'types/base/models/item-dto'; 387 + import type { ListOptions } from 'types/listOptions'; 388 + 389 + @@ -11,6 +12,7 @@ interface UseListProps { 390 + } 391 + 392 + function useList({ item, listOptions }: UseListProps) { 393 + + const { enableBlurUnplayedTitle, enableBlurUnplayedDescription } = useUserSettings(); 394 + const action = listOptions.action ?? 'link'; 395 + const isLargeStyle = listOptions.imageSize === 'large'; 396 + const enableOverview = listOptions.enableOverview; 397 + @@ -53,7 +55,9 @@ function useList({ item, listOptions }: UseListProps) { 398 + className: listWrapperClass, 399 + title: item.Name, 400 + action, 401 + - dataAttributes 402 + + dataAttributes, 403 + + enableBlurUnplayedTitle, 404 + + enableBlurUnplayedDescription 405 + }); 406 + 407 + const getListContentProps = () => ({ 408 + diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js 409 + index 5b6c54c2bf..33b83d429a 100644 410 + --- a/src/components/listview/listview.js 411 + +++ b/src/components/listview/listview.js 412 + @@ -126,7 +126,7 @@ function getChannelImageUrl(item, size) { 413 + } 414 + } 415 + 416 + -function getTextLinesHtml(textlines, isLargeStyle) { 417 + +function getTextLinesHtml(textlines, isLargeStyle, blurUnplayed) { 418 + let html = ''; 419 + 420 + const largeTitleTagName = layoutManager.tv ? 'h2' : 'div'; 421 + @@ -151,6 +151,10 @@ function getTextLinesHtml(textlines, isLargeStyle) { 422 + 423 + elem.classList.add('listItemBodyText'); 424 + 425 + + if (blurUnplayed) { 426 + + elem.classList.add('listItemBodyText-blurred'); 427 + + } 428 + + 429 + elem.innerHTML = '<bdi>' + escapeHtml(text) + '</bdi>'; 430 + 431 + html += elem.outerHTML; 432 + @@ -171,7 +175,7 @@ function getRightButtonsHtml(options) { 433 + return html; 434 + } 435 + 436 + -export function getListViewHtml(options) { 437 + +export function getListViewHtml(options, enableBlurUnplayedTitle, enableBlurUnplayedDescription) { 438 + const items = options.items; 439 + 440 + let groupTitle = ''; 441 + @@ -401,7 +405,11 @@ export function getListViewHtml(options) { 442 + 443 + html += `<div class="${cssClass}">`; 444 + 445 + - html += getTextLinesHtml(textlines, isLargeStyle); 446 + + const isPlayed = Boolean(item.UserData?.Played); 447 + + const blurUnplayedTitle = enableBlurUnplayedTitle && !isPlayed; 448 + + const blurUnplayedDescription = enableBlurUnplayedDescription && !isPlayed; 449 + + 450 + + html += getTextLinesHtml(textlines, isLargeStyle, blurUnplayedTitle); 451 + 452 + if (options.mediaInfo !== false && !enableSideMediaInfo) { 453 + const mediaInfoClass = 'secondary listItemMediaInfo listItemBodyText'; 454 + @@ -419,7 +427,11 @@ export function getListViewHtml(options) { 455 + if (enableOverview && item.Overview) { 456 + // eslint-disable-next-line sonarjs/disabled-auto-escaping 457 + const overview = DOMPurify.sanitize(markdownIt({ html: true }).render(item.Overview || '')); 458 + - html += '<div class="secondary listItem-overview listItemBodyText">'; 459 + + html += '<div class="secondary listItem-overview listItemBodyText'; 460 + + if (blurUnplayedDescription) { 461 + + html += ' listItemBodyText-blurred'; 462 + + } 463 + + html += '">'; 464 + html += '<bdi>' + overview + '</bdi>'; 465 + html += '</div>'; 466 + } 467 + diff --git a/src/components/listview/listview.scss b/src/components/listview/listview.scss 468 + index ce96f93b24..9975677bce 100644 469 + --- a/src/components/listview/listview.scss 470 + +++ b/src/components/listview/listview.scss 471 + @@ -114,6 +114,15 @@ 472 + justify-content: center; 473 + } 474 + 475 + +.listItemBodyText-blurred { 476 + + filter: blur(5px); 477 + + transition: filter 0.3s ease; 478 + +} 479 + + 480 + +.listItem:hover .listItemBodyText-blurred { 481 + + filter: blur(0); 482 + +} 483 + + 484 + .layout-tv .listItemBody { 485 + padding: 0.35em 0.75em; 486 + } 487 + diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js 488 + index bbef1931bc..4ddca88e7d 100644 489 + --- a/src/components/remotecontrol/remotecontrol.js 490 + +++ b/src/components/remotecontrol/remotecontrol.js 491 + @@ -474,7 +474,10 @@ export default function () { 492 + id: 'remove' 493 + }], 494 + dragHandle: true 495 + - }); 496 + + }, 497 + + userSettings.enableBlurUnplayedTitle(), 498 + + userSettings.enableBlurUnplayedDescription() 499 + + ); 500 + 501 + const itemsContainer = context.querySelector('.playlist'); 502 + let focusedItemPlaylistId = itemsContainer.querySelector('button:focus'); 503 + diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js 504 + index e425285050..5ea2ac9820 100644 505 + --- a/src/controllers/itemDetails/index.js 506 + +++ b/src/controllers/itemDetails/index.js 507 + @@ -126,7 +126,7 @@ function getProgramScheduleHtml(items, action = 'none') { 508 + action, 509 + moreButton: false, 510 + recordButton: false 511 + - }); 512 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 513 + } 514 + 515 + function getSelectedMediaSource(page, mediaSources) { 516 + @@ -801,7 +801,9 @@ function renderNextUp(page, item, user) { 517 + displayAsSpecial: item.Type == 'Season' && item.IndexNumber, 518 + overlayText: false, 519 + centerText: true, 520 + - overlayPlayButton: true 521 + + overlayPlayButton: true, 522 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 523 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 524 + }); 525 + const itemsContainer = section.querySelector('.nextUpItems'); 526 + itemsContainer.innerHTML = html; 527 + @@ -1413,7 +1415,7 @@ function renderChildren(page, item) { 528 + image: false, 529 + artist: showArtist, 530 + containerAlbumArtists: item.AlbumArtists 531 + - }); 532 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 533 + isList = true; 534 + } else if (item.Type == 'Series') { 535 + scrollX = enableScrollX(); 536 + @@ -1462,7 +1464,7 @@ function renderChildren(page, item) { 537 + action: !layoutManager.desktop ? 'link' : 'none', 538 + imagePlayButton: true, 539 + includeParentInfoInTitle: false 540 + - }); 541 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 542 + } 543 + } 544 + 545 + @@ -1575,7 +1577,7 @@ function renderProgramsForChannel(page, result) { 546 + showProgramTime: true, 547 + mediaInfo: false, 548 + parentTitleWithTitle: true 549 + - }) + '</div></div>'; 550 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()) + '</div></div>'; 551 + } 552 + 553 + currentStartDate = itemStartDate; 554 + @@ -1600,7 +1602,7 @@ function renderProgramsForChannel(page, result) { 555 + showProgramTime: true, 556 + mediaInfo: false, 557 + parentTitleWithTitle: true 558 + - }) + '</div></div>'; 559 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()) + '</div></div>'; 560 + } 561 + 562 + page.querySelector('.programGuide').innerHTML = html; 563 + diff --git a/src/controllers/list.js b/src/controllers/list.js 564 + index 1e2a77fbc2..57308ba33c 100644 565 + --- a/src/controllers/list.js 566 + +++ b/src/controllers/list.js 567 + @@ -516,11 +516,13 @@ class ItemsView { 568 + 569 + function getItemsHtml(items) { 570 + const settings = self.getViewSettings(); 571 + + const enableBlurUnplayedTitle = settings.enableBlurUnplayedTitle; 572 + + const enableBlurUnplayedDescription = settings.enableBlurUnplayedDescription; 573 + 574 + if (settings.imageType === 'list') { 575 + return listView.getListViewHtml({ 576 + items: items 577 + - }); 578 + + }, enableBlurUnplayedTitle, enableBlurUnplayedDescription); 579 + } 580 + 581 + let shape; 582 + @@ -1215,6 +1217,8 @@ class ItemsView { 583 + const params = this.params; 584 + const item = this.currentItem; 585 + let showTitle = userSettings.get(basekey + '-showTitle'); 586 + + const enableBlurUnplayedTitle = userSettings.enableBlurUnplayedTitle(); 587 + + const enableBlurUnplayedDescription = userSettings.enableBlurUnplayedDescription(); 588 + 589 + if (showTitle === 'true') { 590 + showTitle = true; 591 + @@ -1240,7 +1244,9 @@ class ItemsView { 592 + showTitle, 593 + showYear: userSettings.get(basekey + '-showYear') !== 'false', 594 + imageType: imageType || 'primary', 595 + - viewType: userSettings.get(basekey + '-viewType') || 'images' 596 + + viewType: userSettings.get(basekey + '-viewType') || 'images', 597 + + enableBlurUnplayedTitle, 598 + + enableBlurUnplayedDescription 599 + }; 600 + } 601 + 602 + diff --git a/src/controllers/shows/episodes.js b/src/controllers/shows/episodes.js 603 + index 3fdeefcda8..10e308f301 100644 604 + --- a/src/controllers/shows/episodes.js 605 + +++ b/src/controllers/shows/episodes.js 606 + @@ -114,7 +114,7 @@ export default function (view, params, tabContent) { 607 + items: result.Items, 608 + sortBy: query.SortBy, 609 + showParentTitle: true 610 + - }); 611 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 612 + } else if (viewStyle == 'PosterCard') { 613 + html = cardBuilder.getCardsHtml({ 614 + items: result.Items, 615 + @@ -245,4 +245,3 @@ export default function (view, params, tabContent) { 616 + reloadItems(tabContent); 617 + }; 618 + } 619 + - 620 + diff --git a/src/controllers/shows/tvshows.js b/src/controllers/shows/tvshows.js 621 + index 6ffdfa1dfd..99f29855b1 100644 622 + --- a/src/controllers/shows/tvshows.js 623 + +++ b/src/controllers/shows/tvshows.js 624 + @@ -141,7 +141,7 @@ export default function (view, params, tabContent) { 625 + items: result.Items, 626 + context: 'tvshows', 627 + sortBy: query.SortBy 628 + - }); 629 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 630 + } else if (viewStyle == 'PosterCard') { 631 + html = cardBuilder.getCardsHtml({ 632 + items: result.Items, 633 + @@ -302,4 +302,3 @@ export default function (view, params, tabContent) { 634 + this.alphaPicker?.updateControls(getQuery()); 635 + }; 636 + } 637 + - 638 + diff --git a/src/hooks/useUserSettings.tsx b/src/hooks/useUserSettings.tsx 639 + index 289588669e..d1ddec2615 100644 640 + --- a/src/hooks/useUserSettings.tsx 641 + +++ b/src/hooks/useUserSettings.tsx 642 + @@ -13,6 +13,8 @@ interface UserSettings { 643 + dashboardTheme?: string 644 + dateTimeLocale?: string 645 + language?: string 646 + + enableBlurUnplayedTitle?: boolean; 647 + + enableBlurUnplayedDescription?: boolean; 648 + } 649 + 650 + // NOTE: This is an incomplete list of only the settings that are currently being used 651 + @@ -25,7 +27,10 @@ const UserSettingField = { 652 + DashboardTheme: 'dashboardTheme', 653 + // Locale settings 654 + DateTimeLocale: 'datetimelocale', 655 + - Language: 'language' 656 + + Language: 'language', 657 + + // Display 658 + + enableBlurUnplayedTitle: 'BlurUnplayedTitle', 659 + + enableBlurUnplayedDescription: 'BlurUnplayedDescription' 660 + }; 661 + 662 + const UserSettingsContext = createContext<UserSettings>({ 663 + @@ -41,6 +46,8 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children 664 + const [ dashboardTheme, setDashboardTheme ] = useState<string>(); 665 + const [ dateTimeLocale, setDateTimeLocale ] = useState<string>(); 666 + const [ language, setLanguage ] = useState<string | undefined>(FALLBACK_CULTURE); 667 + + const [ enableBlurUnplayedTitle, setEnableBlurUnplayedTitle ] = useState<boolean>(); 668 + + const [ enableBlurUnplayedDescription, setEnableBlurUnplayedDescription ] = useState<boolean>(); 669 + 670 + const { user } = useApi(); 671 + 672 + @@ -50,14 +57,18 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children 673 + theme, 674 + dashboardTheme, 675 + dateTimeLocale, 676 + - locale: language 677 + + language, 678 + + enableBlurUnplayedTitle, 679 + + enableBlurUnplayedDescription 680 + }), [ 681 + customCss, 682 + disableCustomCss, 683 + theme, 684 + dashboardTheme, 685 + dateTimeLocale, 686 + - language 687 + + language, 688 + + enableBlurUnplayedTitle, 689 + + enableBlurUnplayedDescription 690 + ]); 691 + 692 + // Update the values of the user settings 693 + @@ -68,6 +79,8 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children 694 + setDashboardTheme(userSettings.dashboardTheme()); 695 + setDateTimeLocale(userSettings.dateTimeLocale()); 696 + setLanguage(userSettings.language()); 697 + + setEnableBlurUnplayedTitle(userSettings.enableBlurUnplayedTitle()); 698 + + setEnableBlurUnplayedDescription(userSettings.enableBlurUnplayedDescription()); 699 + }, []); 700 + 701 + const onUserSettingsChange = useCallback((_e: Event, name?: string) => { 702 + diff --git a/src/scripts/itemsByName.js b/src/scripts/itemsByName.js 703 + index f7ab817e3e..32ad8893d6 100644 704 + --- a/src/scripts/itemsByName.js 705 + +++ b/src/scripts/itemsByName.js 706 + @@ -5,6 +5,7 @@ import cardBuilder from 'components/cardbuilder/cardBuilder'; 707 + import imageLoader from 'components/images/imageLoader'; 708 + import globalize from 'lib/globalize'; 709 + import { ServerConnections } from 'lib/jellyfin-apiclient'; 710 + +import * as userSettings from 'scripts/settings/userSettings'; 711 + 712 + import 'elements/emby-itemscontainer/emby-itemscontainer'; 713 + import 'elements/emby-button/emby-button'; 714 + @@ -289,7 +290,7 @@ function loadItems(element, item, type, query, listOptions) { 715 + const itemsContainer = element.querySelector('.itemsContainer'); 716 + 717 + if (type === 'Audio') { 718 + - html = listView.getListViewHtml(listOptions); 719 + + html = listView.getListViewHtml(listOptions, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 720 + itemsContainer.classList.remove('vertical-wrap'); 721 + itemsContainer.classList.add('vertical-list'); 722 + } else { 723 + diff --git a/src/scripts/playlistViewer.js b/src/scripts/playlistViewer.js 724 + index 002b210fc8..bfe45410ee 100644 725 + --- a/src/scripts/playlistViewer.js 726 + +++ b/src/scripts/playlistViewer.js 727 + @@ -2,6 +2,7 @@ import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; 728 + 729 + import listView from 'components/listview/listview'; 730 + import { ServerConnections } from 'lib/jellyfin-apiclient'; 731 + +import * as userSettings from 'scripts/settings/userSettings'; 732 + import { toApi } from 'utils/jellyfin-apiclient/compat'; 733 + 734 + function getFetchPlaylistItemsFn(apiClient, itemId) { 735 + @@ -26,7 +27,7 @@ function getItemsHtmlFn(playlistId, isEditable = false) { 736 + dragHandle: isEditable, 737 + playlistId, 738 + showParentTitle: true 739 + - }); 740 + + }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 741 + }; 742 + } 743 + 744 + diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js 745 + index 439c1df87a..dd9c9b32c0 100644 746 + --- a/src/scripts/settings/userSettings.js 747 + +++ b/src/scripts/settings/userSettings.js 748 + @@ -282,6 +282,32 @@ export class UserSettings { 749 + return toBoolean(this.get('blurhash', false), true); 750 + } 751 + 752 + + /** 753 + + * Get or set 'BlurUnplayed' state. 754 + + * @param {boolean|undefined} [val] - Flag to enable 'BlurUnplayed' or undefined. 755 + + * @return {boolean} 'BlurUnplayed' state. 756 + + */ 757 + + enableBlurUnplayedTitle(val) { 758 + + if (val !== undefined) { 759 + + return this.set('BlurUnplayedTitle', val.toString(), false); 760 + + } 761 + + 762 + + return toBoolean(this.get('BlurUnplayedTitle', false), true); 763 + + } 764 + + 765 + + /** 766 + + * Get or set 'BlurUnplayed' state. 767 + + * @param {boolean|undefined} [val] - Flag to enable 'BlurUnplayed' or undefined. 768 + + * @return {boolean} 'BlurUnplayed' state. 769 + + */ 770 + + enableBlurUnplayedDescription(val) { 771 + + if (val !== undefined) { 772 + + return this.set('BlurUnplayedDescription', val.toString(), false); 773 + + } 774 + + 775 + + return toBoolean(this.get('BlurUnplayedDescription', false), true); 776 + + } 777 + + 778 + /** 779 + * Get or set 'Backdrops' state. 780 + * @param {boolean|undefined} [val] - Flag to enable 'Backdrops' or undefined. 781 + @@ -693,6 +719,8 @@ export const enableThemeSongs = currentSettings.enableThemeSongs.bind(currentSet 782 + export const enableThemeVideos = currentSettings.enableThemeVideos.bind(currentSettings); 783 + export const enableFastFadein = currentSettings.enableFastFadein.bind(currentSettings); 784 + export const enableBlurhash = currentSettings.enableBlurhash.bind(currentSettings); 785 + +export const enableBlurUnplayedTitle = currentSettings.enableBlurUnplayedTitle.bind(currentSettings); 786 + +export const enableBlurUnplayedDescription = currentSettings.enableBlurUnplayedDescription.bind(currentSettings); 787 + export const enableBackdrops = currentSettings.enableBackdrops.bind(currentSettings); 788 + export const detailsBanner = currentSettings.detailsBanner.bind(currentSettings); 789 + export const useEpisodeImagesInNextUpAndResume = currentSettings.useEpisodeImagesInNextUpAndResume.bind(currentSettings); 790 + diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json 791 + index 90e98205ef..d66e23df5b 100644 792 + --- a/src/strings/en-gb.json 793 + +++ b/src/strings/en-gb.json 794 + @@ -205,6 +205,10 @@ 795 + "EditMetadata": "Edit metadata", 796 + "EditSubtitles": "Edit subtitles", 797 + "EnableBackdropsHelp": "Display the backdrops in the background of some pages while browsing the library.", 798 + + "EnableBlurUnplayedTitle": "Enable blurred title for unplayed episodes", 799 + + "EnableBlurUnplayedTitleHelp": "Hide episode titles until played or hovered", 800 + + "EnableBlurUnplayedDescription": "Enable blurred description for unplayed episodes", 801 + + "EnableBlurUnplayedDescriptionHelp": "Hide episode descriptions until played or hovered", 802 + "EnableCinemaMode": "Cinema mode", 803 + "EnableDisplayMirroring": "Display mirroring", 804 + "EnableExternalVideoPlayers": "External video players", 805 + diff --git a/src/strings/en-us.json b/src/strings/en-us.json 806 + index 28fbf0b55f..8d939e66e5 100644 807 + --- a/src/strings/en-us.json 808 + +++ b/src/strings/en-us.json 809 + @@ -275,6 +275,10 @@ 810 + "EnableBackdropsHelp": "Display the backdrops in the background of some pages while browsing the library.", 811 + "EnableBlurHash": "Enable blurred placeholders for images", 812 + "EnableBlurHashHelp": "Images that are still being loaded will be displayed with a unique placeholder.", 813 + + "EnableBlurUnplayedTitle": "Enable blurred title for unplayed episodes", 814 + + "EnableBlurUnplayedTitleHelp": "Hide episode titles until played or hovered", 815 + + "EnableBlurUnplayedDescription": "Enable blurred description for unplayed episodes", 816 + + "EnableBlurUnplayedDescriptionHelp": "Hide episode descriptions until played or hovered", 817 + "EnableCinemaMode": "Cinema mode", 818 + "EnableColorCodedBackgrounds": "Color coded backgrounds", 819 + "EnableDecodingColorDepth10Hevc": "Enable 10-bit hardware decoding for HEVC", 820 + diff --git a/src/strings/es-ar.json b/src/strings/es-ar.json 821 + index 6e24025d3a..48fc7bc724 100644 822 + --- a/src/strings/es-ar.json 823 + +++ b/src/strings/es-ar.json 824 + @@ -200,6 +200,10 @@ 825 + "AuthProviderHelp": "Seleccioná un proveedor de autenticación que se va a utilizar para autenticar la contraseña de este usuario.", 826 + "CriticRating": "Calificación de los críticos", 827 + "DefaultSubtitlesHelp": "Los subtítulos se cargan según los indicadores predeterminados y forzados en los metadatos incrustados. Las preferencias de idioma se consideran cuando hay varias opciones disponibles.", 828 + + "EnableBlurUnplayedTitle": "Activar título difuminado para episodios no reproducidos", 829 + + "EnableBlurUnplayedTitleHelp": "Oculta los títulos de los episodios hasta que se reproduzcan o se pase el cursor", 830 + + "EnableBlurUnplayedDescription": "Activar descripción difuminada para episodios no reproducidos", 831 + + "EnableBlurUnplayedDescriptionHelp": "Oculta la descripción de los episodios hasta que se reproduzcan o se pase el cursor", 832 + "EnableDisplayMirroring": "Habilitar duplicación de la pantalla", 833 + "EnableExternalVideoPlayers": "Reproductores de video externos", 834 + "EnableExternalVideoPlayersHelp": "Se va a mostrar un menú de reproductor externo al iniciar la reproducción de video.", 835 + diff --git a/src/strings/es-mx.json b/src/strings/es-mx.json 836 + index 93cdcff6ba..d4b4e718e7 100644 837 + --- a/src/strings/es-mx.json 838 + +++ b/src/strings/es-mx.json 839 + @@ -163,6 +163,10 @@ 840 + "EnableBackdropsHelp": "Mostrar imágenes de fondo en el fondo de algunas páginas mientras navega por la biblioteca.", 841 + "EnableCinemaMode": "Modo cine", 842 + "EnableColorCodedBackgrounds": "Fondos de colores codificados", 843 + + "EnableBlurUnplayedTitle": "Activar título difuminado para episodios no reproducidos", 844 + + "EnableBlurUnplayedTitleHelp": "Oculta los títulos de los episodios hasta que se reproduzcan o se pase el cursor", 845 + + "EnableBlurUnplayedDescription": "Activar descripción difuminada para episodios no reproducidos", 846 + + "EnableBlurUnplayedDescriptionHelp": "Oculta la descripción de los episodios hasta que se reproduzcan o se pase el cursor", 847 + "EnableDisplayMirroring": "Duplicado de pantalla", 848 + "EnableExternalVideoPlayers": "Reproductores de video externos", 849 + "EnableExternalVideoPlayersHelp": "Un menú de reproductor externo se mostrara cuando inicie la reproducción de un video.", 850 + diff --git a/src/strings/es.json b/src/strings/es.json 851 + index 3a9a951429..2956e701e9 100644 852 + --- a/src/strings/es.json 853 + +++ b/src/strings/es.json 854 + @@ -132,6 +132,10 @@ 855 + "EditImages": "Editar imágenes", 856 + "EditSubtitles": "Editar subtítulos", 857 + "EnableCinemaMode": "Modo cine", 858 + + "EnableBlurUnplayedTitle": "Activar título difuminado para episodios no reproducidos", 859 + + "EnableBlurUnplayedTitleHelp": "Oculta los títulos de los episodios hasta que se reproduzcan o se pase el cursor", 860 + + "EnableBlurUnplayedDescription": "Activar descripción difuminada para episodios no reproducidos", 861 + + "EnableBlurUnplayedDescriptionHelp": "Oculta la descripción de los episodios hasta que se reproduzcan o se pase el cursor", 862 + "EnableDisplayMirroring": "Mirroring de la pantalla", 863 + "EnableExternalVideoPlayers": "Reproductores externos", 864 + "EnableHardwareEncoding": "Activar codificación por hardware", 865 + diff --git a/src/types/listOptions.ts b/src/types/listOptions.ts 866 + index 4de41fd353..2ff393a0ef 100644 867 + --- a/src/types/listOptions.ts 868 + +++ b/src/types/listOptions.ts 869 + @@ -39,4 +39,6 @@ export interface ListOptions extends TextLineOpts { 870 + enableRatingButton?: boolean; 871 + smallIcon?: boolean; 872 + sortBy?: ItemSortBy; 873 + + enableBlurUnplayedTitle?: boolean; 874 + + enableBlurUnplayedDescription?: boolean; 875 + } 876 + -- 877 + 2.53.0 878 +
+56
patches/jellyfin-web/0002-remove-blur-audio-title.patch
··· 1 + From d2a6d7406c280c551ad80fa6505581c06fb7e63b Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Natal=C3=AD=20Paura?= 3 + <30585029+natilou@users.noreply.github.com> 4 + Date: Sat, 3 Jan 2026 21:25:13 -0300 5 + Subject: [PATCH 2/5] remove blur audio title 6 + 7 + --- 8 + src/scripts/itemsByName.js | 3 +-- 9 + src/scripts/playlistViewer.js | 3 +-- 10 + 2 files changed, 2 insertions(+), 4 deletions(-) 11 + 12 + diff --git a/src/scripts/itemsByName.js b/src/scripts/itemsByName.js 13 + index 32ad8893d6..f7ab817e3e 100644 14 + --- a/src/scripts/itemsByName.js 15 + +++ b/src/scripts/itemsByName.js 16 + @@ -5,7 +5,6 @@ import cardBuilder from 'components/cardbuilder/cardBuilder'; 17 + import imageLoader from 'components/images/imageLoader'; 18 + import globalize from 'lib/globalize'; 19 + import { ServerConnections } from 'lib/jellyfin-apiclient'; 20 + -import * as userSettings from 'scripts/settings/userSettings'; 21 + 22 + import 'elements/emby-itemscontainer/emby-itemscontainer'; 23 + import 'elements/emby-button/emby-button'; 24 + @@ -290,7 +289,7 @@ function loadItems(element, item, type, query, listOptions) { 25 + const itemsContainer = element.querySelector('.itemsContainer'); 26 + 27 + if (type === 'Audio') { 28 + - html = listView.getListViewHtml(listOptions, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 29 + + html = listView.getListViewHtml(listOptions); 30 + itemsContainer.classList.remove('vertical-wrap'); 31 + itemsContainer.classList.add('vertical-list'); 32 + } else { 33 + diff --git a/src/scripts/playlistViewer.js b/src/scripts/playlistViewer.js 34 + index bfe45410ee..002b210fc8 100644 35 + --- a/src/scripts/playlistViewer.js 36 + +++ b/src/scripts/playlistViewer.js 37 + @@ -2,7 +2,6 @@ import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; 38 + 39 + import listView from 'components/listview/listview'; 40 + import { ServerConnections } from 'lib/jellyfin-apiclient'; 41 + -import * as userSettings from 'scripts/settings/userSettings'; 42 + import { toApi } from 'utils/jellyfin-apiclient/compat'; 43 + 44 + function getFetchPlaylistItemsFn(apiClient, itemId) { 45 + @@ -27,7 +26,7 @@ function getItemsHtmlFn(playlistId, isEditable = false) { 46 + dragHandle: isEditable, 47 + playlistId, 48 + showParentTitle: true 49 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 50 + + }); 51 + }; 52 + } 53 + 54 + -- 55 + 2.53.0 56 +
+183
patches/jellyfin-web/0003-refactor-getListViewHtml-to-receive-new-settings-in-.patch
··· 1 + From 6ee536d65ba0b2d1ed7672af3a9a622028e0683c Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Natal=C3=AD=20Paura?= 3 + <30585029+natilou@users.noreply.github.com> 4 + Date: Sat, 3 Jan 2026 21:32:24 -0300 5 + Subject: [PATCH 3/5] refactor getListViewHtml to receive new settings in 6 + options object 7 + 8 + --- 9 + src/components/listview/listview.js | 6 ++-- 10 + src/components/remotecontrol/remotecontrol.js | 9 +++--- 11 + src/controllers/itemDetails/index.js | 30 ++++++++++++------- 12 + src/controllers/list.js | 6 ++-- 13 + src/controllers/shows/episodes.js | 6 ++-- 14 + src/controllers/shows/tvshows.js | 6 ++-- 15 + 6 files changed, 39 insertions(+), 24 deletions(-) 16 + 17 + diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js 18 + index 33b83d429a..bce459a629 100644 19 + --- a/src/components/listview/listview.js 20 + +++ b/src/components/listview/listview.js 21 + @@ -175,7 +175,7 @@ function getRightButtonsHtml(options) { 22 + return html; 23 + } 24 + 25 + -export function getListViewHtml(options, enableBlurUnplayedTitle, enableBlurUnplayedDescription) { 26 + +export function getListViewHtml(options) { 27 + const items = options.items; 28 + 29 + let groupTitle = ''; 30 + @@ -406,8 +406,8 @@ export function getListViewHtml(options, enableBlurUnplayedTitle, enableBlurUnpl 31 + html += `<div class="${cssClass}">`; 32 + 33 + const isPlayed = Boolean(item.UserData?.Played); 34 + - const blurUnplayedTitle = enableBlurUnplayedTitle && !isPlayed; 35 + - const blurUnplayedDescription = enableBlurUnplayedDescription && !isPlayed; 36 + + const blurUnplayedTitle = options.enableBlurUnplayedTitle && !isPlayed; 37 + + const blurUnplayedDescription = options.enableBlurUnplayedDescription && !isPlayed; 38 + 39 + html += getTextLinesHtml(textlines, isLargeStyle, blurUnplayedTitle); 40 + 41 + diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js 42 + index 4ddca88e7d..33c643a555 100644 43 + --- a/src/components/remotecontrol/remotecontrol.js 44 + +++ b/src/components/remotecontrol/remotecontrol.js 45 + @@ -473,11 +473,10 @@ export default function () { 46 + title: globalize.translate('ButtonRemove'), 47 + id: 'remove' 48 + }], 49 + - dragHandle: true 50 + - }, 51 + - userSettings.enableBlurUnplayedTitle(), 52 + - userSettings.enableBlurUnplayedDescription() 53 + - ); 54 + + dragHandle: true, 55 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 56 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 57 + + }); 58 + 59 + const itemsContainer = context.querySelector('.playlist'); 60 + let focusedItemPlaylistId = itemsContainer.querySelector('button:focus'); 61 + diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js 62 + index 5ea2ac9820..4e80f00b8f 100644 63 + --- a/src/controllers/itemDetails/index.js 64 + +++ b/src/controllers/itemDetails/index.js 65 + @@ -125,8 +125,10 @@ function getProgramScheduleHtml(items, action = 'none') { 66 + runtime: false, 67 + action, 68 + moreButton: false, 69 + - recordButton: false 70 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 71 + + recordButton: false, 72 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 73 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 74 + + }); 75 + } 76 + 77 + function getSelectedMediaSource(page, mediaSources) { 78 + @@ -1414,8 +1416,10 @@ function renderChildren(page, item) { 79 + action: 'playallfromhere', 80 + image: false, 81 + artist: showArtist, 82 + - containerAlbumArtists: item.AlbumArtists 83 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 84 + + containerAlbumArtists: item.AlbumArtists, 85 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 86 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 87 + + }); 88 + isList = true; 89 + } else if (item.Type == 'Series') { 90 + scrollX = enableScrollX(); 91 + @@ -1463,8 +1467,10 @@ function renderChildren(page, item) { 92 + highlight: false, 93 + action: !layoutManager.desktop ? 'link' : 'none', 94 + imagePlayButton: true, 95 + - includeParentInfoInTitle: false 96 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 97 + + includeParentInfoInTitle: false, 98 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 99 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 100 + + }); 101 + } 102 + } 103 + 104 + @@ -1576,8 +1582,10 @@ function renderProgramsForChannel(page, result) { 105 + image: false, 106 + showProgramTime: true, 107 + mediaInfo: false, 108 + - parentTitleWithTitle: true 109 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()) + '</div></div>'; 110 + + parentTitleWithTitle: true, 111 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 112 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 113 + + }) + '</div></div>'; 114 + } 115 + 116 + currentStartDate = itemStartDate; 117 + @@ -1601,8 +1609,10 @@ function renderProgramsForChannel(page, result) { 118 + image: false, 119 + showProgramTime: true, 120 + mediaInfo: false, 121 + - parentTitleWithTitle: true 122 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()) + '</div></div>'; 123 + + parentTitleWithTitle: true, 124 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 125 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 126 + + }) + '</div></div>'; 127 + } 128 + 129 + page.querySelector('.programGuide').innerHTML = html; 130 + diff --git a/src/controllers/list.js b/src/controllers/list.js 131 + index 57308ba33c..79dfabbb12 100644 132 + --- a/src/controllers/list.js 133 + +++ b/src/controllers/list.js 134 + @@ -521,8 +521,10 @@ class ItemsView { 135 + 136 + if (settings.imageType === 'list') { 137 + return listView.getListViewHtml({ 138 + - items: items 139 + - }, enableBlurUnplayedTitle, enableBlurUnplayedDescription); 140 + + items: items, 141 + + enableBlurUnplayedTitle, 142 + + enableBlurUnplayedDescription 143 + + }); 144 + } 145 + 146 + let shape; 147 + diff --git a/src/controllers/shows/episodes.js b/src/controllers/shows/episodes.js 148 + index 10e308f301..a8458762ff 100644 149 + --- a/src/controllers/shows/episodes.js 150 + +++ b/src/controllers/shows/episodes.js 151 + @@ -113,8 +113,10 @@ export default function (view, params, tabContent) { 152 + html = listView.getListViewHtml({ 153 + items: result.Items, 154 + sortBy: query.SortBy, 155 + - showParentTitle: true 156 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 157 + + showParentTitle: true, 158 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 159 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 160 + + }); 161 + } else if (viewStyle == 'PosterCard') { 162 + html = cardBuilder.getCardsHtml({ 163 + items: result.Items, 164 + diff --git a/src/controllers/shows/tvshows.js b/src/controllers/shows/tvshows.js 165 + index 99f29855b1..e7807ab9ce 100644 166 + --- a/src/controllers/shows/tvshows.js 167 + +++ b/src/controllers/shows/tvshows.js 168 + @@ -140,8 +140,10 @@ export default function (view, params, tabContent) { 169 + html = listView.getListViewHtml({ 170 + items: result.Items, 171 + context: 'tvshows', 172 + - sortBy: query.SortBy 173 + - }, userSettings.enableBlurUnplayedTitle(), userSettings.enableBlurUnplayedDescription()); 174 + + sortBy: query.SortBy, 175 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 176 + + enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 177 + + }); 178 + } else if (viewStyle == 'PosterCard') { 179 + html = cardBuilder.getCardsHtml({ 180 + items: result.Items, 181 + -- 182 + 2.53.0 183 +
+112
patches/jellyfin-web/0004-blur-title-and-description-in-episode-details-and-Mo.patch
··· 1 + From 4b80006c76d0f0bff6b155570d1a9562c57ef565 Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Natal=C3=AD=20Paura?= 3 + <30585029+natilou@users.noreply.github.com> 4 + Date: Sun, 4 Jan 2026 11:57:36 -0300 5 + Subject: [PATCH 4/5] blur title and description in episode details and More 6 + from Season section 7 + 8 + --- 9 + src/controllers/itemDetails/index.js | 27 ++++++++++++++++++--------- 10 + src/styles/librarybrowser.scss | 9 +++++++++ 11 + 2 files changed, 27 insertions(+), 9 deletions(-) 12 + 13 + diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js 14 + index 4e80f00b8f..986958685b 100644 15 + --- a/src/controllers/itemDetails/index.js 16 + +++ b/src/controllers/itemDetails/index.js 17 + @@ -477,7 +477,11 @@ function renderName(item, container, context) { 18 + 19 + if (html && !parentNameLast) { 20 + if (tvSeasonHtml) { 21 + - html += '<h3 class="itemName infoText subtitle focuscontainer-x"><bdi>' + tvSeasonHtml + ' - ' + name + '</bdi></h3>'; 22 + + html += '<h3 class="itemName infoText subtitle focuscontainer-x'; 23 + + if (!item.UserData.Played && userSettings.enableBlurUnplayedTitle()) { 24 + + html += ' listItemBodyText-blurred'; 25 + + } 26 + + html += '"><bdi>' + tvSeasonHtml + ' - ' + name + '</bdi></h3>'; 27 + } else { 28 + html += '<h3 class="itemName infoText subtitle"><bdi>' + name + '</bdi></h3>'; 29 + } 30 + @@ -921,6 +925,10 @@ function renderOverview(page, item) { 31 + for (const anchor of overviewElemnt.querySelectorAll('a')) { 32 + anchor.setAttribute('target', '_blank'); 33 + } 34 + + 35 + + if (item.Type === 'Episode' && !item.UserData.Played && userSettings.enableBlurUnplayedDescription()) { 36 + + overviewElemnt.classList.add('listItemBodyText-blurred'); 37 + + } 38 + } 39 + } else { 40 + for (const overviewElemnt of overviewElements) { 41 + @@ -1179,7 +1187,8 @@ function renderMoreFromSeason(view, item, apiClient) { 42 + overlayText: false, 43 + centerText: true, 44 + includeParentInfoInTitle: false, 45 + - allowBottomPadding: false 46 + + allowBottomPadding: false, 47 + + enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle() 48 + }); 49 + const card = itemsContainer.querySelector('.card[data-id="' + item.Id + '"]'); 50 + 51 + @@ -1416,9 +1425,7 @@ function renderChildren(page, item) { 52 + action: 'playallfromhere', 53 + image: false, 54 + artist: showArtist, 55 + - containerAlbumArtists: item.AlbumArtists, 56 + - enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 57 + - enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 58 + + containerAlbumArtists: item.AlbumArtists 59 + }); 60 + isList = true; 61 + } else if (item.Type == 'Series') { 62 + @@ -1562,6 +1569,8 @@ function renderProgramsForChannel(page, result) { 63 + let html = ''; 64 + let currentItems = []; 65 + let currentStartDate = null; 66 + + const enableBlurUnplayedTitle = userSettings.enableBlurUnplayedTitle(); 67 + + const enableBlurUnplayedDescription = userSettings.enableBlurUnplayedDescription(); 68 + 69 + for (let i = 0, length = result.Items.length; i < length; i++) { 70 + const item = result.Items[i]; 71 + @@ -1583,8 +1592,8 @@ function renderProgramsForChannel(page, result) { 72 + showProgramTime: true, 73 + mediaInfo: false, 74 + parentTitleWithTitle: true, 75 + - enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 76 + - enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 77 + + enableBlurUnplayedTitle: enableBlurUnplayedTitle, 78 + + enableBlurUnplayedDescription: enableBlurUnplayedDescription 79 + }) + '</div></div>'; 80 + } 81 + 82 + @@ -1610,8 +1619,8 @@ function renderProgramsForChannel(page, result) { 83 + showProgramTime: true, 84 + mediaInfo: false, 85 + parentTitleWithTitle: true, 86 + - enableBlurUnplayedTitle: userSettings.enableBlurUnplayedTitle(), 87 + - enableBlurUnplayedDescription: userSettings.enableBlurUnplayedDescription() 88 + + enableBlurUnplayedTitle: enableBlurUnplayedTitle, 89 + + enableBlurUnplayedDescription: enableBlurUnplayedDescription 90 + }) + '</div></div>'; 91 + } 92 + 93 + diff --git a/src/styles/librarybrowser.scss b/src/styles/librarybrowser.scss 94 + index c43db18321..9936d2d400 100644 95 + --- a/src/styles/librarybrowser.scss 96 + +++ b/src/styles/librarybrowser.scss 97 + @@ -1537,3 +1537,12 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { 98 + padding-left: 0.8em; 99 + padding-right: 0.8em; 100 + } 101 + + 102 + +.listItemBodyText-blurred{ 103 + + filter: blur(5px); 104 + + transition: filter 0.3s ease; 105 + +} 106 + + 107 + +.listItemBodyText-blurred:hover{ 108 + + filter: blur(0); 109 + +} 110 + -- 111 + 2.53.0 112 +
+31
patches/jellyfin-web/0005-fix-lint.patch
··· 1 + From 89800c043650adff4e8700c55f64da64d0427aee Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Natal=C3=AD=20Paura?= 3 + <30585029+natilou@users.noreply.github.com> 4 + Date: Sun, 4 Jan 2026 12:08:17 -0300 5 + Subject: [PATCH 5/5] fix lint 6 + 7 + --- 8 + src/styles/librarybrowser.scss | 4 ++-- 9 + 1 file changed, 2 insertions(+), 2 deletions(-) 10 + 11 + diff --git a/src/styles/librarybrowser.scss b/src/styles/librarybrowser.scss 12 + index 9936d2d400..33398e6838 100644 13 + --- a/src/styles/librarybrowser.scss 14 + +++ b/src/styles/librarybrowser.scss 15 + @@ -1538,11 +1538,11 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { 16 + padding-right: 0.8em; 17 + } 18 + 19 + -.listItemBodyText-blurred{ 20 + +.listItemBodyText-blurred { 21 + filter: blur(5px); 22 + transition: filter 0.3s ease; 23 + } 24 + 25 + -.listItemBodyText-blurred:hover{ 26 + +.listItemBodyText-blurred:hover { 27 + filter: blur(0); 28 + } 29 + -- 30 + 2.53.0 31 +