From 5db40d03ac19d2db51f7e459e7b3fdfc32560a6f Mon Sep 17 00:00:00 2001 From: Dante Tyler <132313624+HaloCelsius@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:23:50 +1000 Subject: [PATCH 01/10] Fix quality secondary text incoherency Parse correct context to the quality options object. This correctly returns the selected option name at index 0 --- src/components/playback/playersettingsmenu.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/playback/playersettingsmenu.js b/src/components/playback/playersettingsmenu.js index 5b0aca301f..12b2488fc3 100644 --- a/src/components/playback/playersettingsmenu.js +++ b/src/components/playback/playersettingsmenu.js @@ -93,12 +93,16 @@ function getQualitySecondaryText(player) { return stream.Type === 'Video'; })[0]; + const videoCodec = videoStream ? videoStream.Codec : null; + const videoBitRate = videoStream ? videoStream.BitRate : null; const videoWidth = videoStream ? videoStream.Width : null; const videoHeight = videoStream ? videoStream.Height : null; const options = qualityoptions.getVideoQualityOptions({ currentMaxBitrate: playbackManager.getMaxStreamingBitrate(player), isAutomaticBitrateEnabled: playbackManager.enableAutomaticBitrateDetection(player), + videoCodec, + videoBitRate, videoWidth: videoWidth, videoHeight: videoHeight, enableAuto: true From e28d70d34cdf162c19e2c432594e947fb2c298b3 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 2 Jan 2026 23:02:49 -0500 Subject: [PATCH 02/10] Enable alphapicker in more sort by options --- src/apps/experimental/components/library/ItemsView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index 3fc2a18c96..0081388602 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -221,9 +221,7 @@ const ItemsView: FC = ({ const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some( (filter) => !!filter ); - const hasSortName = libraryViewSettings.SortBy.includes( - ItemSortBy.SortName - ); + const hasSortName = libraryViewSettings.SortBy !== ItemSortBy.Random; const itemsContainerClass = classNames( 'centered padded-left padded-right padded-right-withalphapicker', From 1459a1132099b32ec3e4080fc3da98c0bc602f86 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 13 Jan 2026 10:26:25 -0500 Subject: [PATCH 03/10] Improve error handling on plugin page --- src/apps/dashboard/routes/plugins/index.tsx | 304 ++++++++++---------- 1 file changed, 153 insertions(+), 151 deletions(-) diff --git a/src/apps/dashboard/routes/plugins/index.tsx b/src/apps/dashboard/routes/plugins/index.tsx index b593f2c845..c6fea6a786 100644 --- a/src/apps/dashboard/routes/plugins/index.tsx +++ b/src/apps/dashboard/routes/plugins/index.tsx @@ -98,164 +98,166 @@ export const Component = () => { className='type-interior mainAnimatedPage' > - {isError ? ( - + - {globalize.translate('PluginsLoadError')} - - ) : ( - - + {globalize.translate('TabPlugins')} + + + + + - - {globalize.translate('TabPlugins')} - - - - - - - - - - - - setStatus(PluginStatusOption.All)} - label={globalize.translate('All')} - /> - - setStatus(PluginStatusOption.Available)} - label={globalize.translate('LabelAvailable')} - /> - - setStatus(PluginStatusOption.Installed)} - label={globalize.translate('LabelInstalled')} - /> - - - - setCategory('')} - label={globalize.translate('All')} - /> - - {Object.values(PluginCategory).map(c => ( - setCategory(c.toLowerCase())} - label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])} - /> - ))} - - - - - - {filteredPlugins.length > 0 ? ( - // NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs - // eslint-disable-next-line @typescript-eslint/no-deprecated - - {filteredPlugins.map(plugin => ( - // NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs - // eslint-disable-next-line @typescript-eslint/no-deprecated - - - - ))} - - ) : ( - - )} + - )} + + {isError ? ( + + {globalize.translate('PluginsLoadError')} + + ) : ( + <> + + + setStatus(PluginStatusOption.All)} + label={globalize.translate('All')} + /> + + setStatus(PluginStatusOption.Available)} + label={globalize.translate('LabelAvailable')} + /> + + setStatus(PluginStatusOption.Installed)} + label={globalize.translate('LabelInstalled')} + /> + + + + setCategory('')} + label={globalize.translate('All')} + /> + + {Object.values(PluginCategory).map(c => ( + setCategory(c.toLowerCase())} + label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])} + /> + ))} + + + + + + {filteredPlugins.length > 0 ? ( + // NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs + // eslint-disable-next-line @typescript-eslint/no-deprecated + + {filteredPlugins.map(plugin => ( + // NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs + // eslint-disable-next-line @typescript-eslint/no-deprecated + + + + ))} + + ) : ( + + )} + + + )} + ); From 264cdafaff250a90a045987b8ef55e84a850a25a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 10 Dec 2025 14:11:19 -0500 Subject: [PATCH 04/10] Add vega os detection --- src/scripts/browser.d.ts | 1 + src/scripts/browser.js | 17 ++++++++++++----- src/scripts/browser.test.ts | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/scripts/browser.d.ts b/src/scripts/browser.d.ts index 3d211441ce..fc24fae03a 100644 --- a/src/scripts/browser.d.ts +++ b/src/scripts/browser.d.ts @@ -21,6 +21,7 @@ declare namespace browser { export let animate: boolean; export let hisense: boolean; export let tizen: boolean; + export let vega: boolean; export let vidaa: boolean; export let web0s: boolean; export let titanos: boolean; diff --git a/src/scripts/browser.js b/src/scripts/browser.js index 0539cd4371..b930f29706 100644 --- a/src/scripts/browser.js +++ b/src/scripts/browser.js @@ -235,10 +235,10 @@ const uaMatch = function (ua) { } return { - browser: browser, - version: version, + browser, + version, platform: platformMatch[0] || '', - versionMajor: versionMajor + versionMajor }; }; @@ -283,10 +283,11 @@ export const detectBrowser = (userAgent = navigator.userAgent) => { browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null; browser.hisense = normalizedUA.includes('hisense'); browser.tizen = normalizedUA.includes('tizen') || window.tizen != null; + browser.vega = normalizedUA.includes('kepler'); browser.vidaa = normalizedUA.includes('vidaa'); browser.web0s = isWeb0s(normalizedUA); - browser.tv = browser.ps4 || browser.xboxOne || isTv(normalizedUA); + browser.tv = browser.ps4 || browser.vega || browser.xboxOne || isTv(normalizedUA); browser.operaTv = browser.tv && normalizedUA.includes('opr/'); browser.edgeUwp = (browser.edge || browser.edgeChromium) && (normalizedUA.includes('msapphost') || normalizedUA.includes('webview')); @@ -305,9 +306,15 @@ export const detectBrowser = (userAgent = navigator.userAgent) => { delete browser.chrome; delete browser.safari; } else if (browser.titanos) { - // UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true + // UserAgent string contains 'Opr' and 'Safari', but we only want 'titanos' to be true delete browser.operaTv; delete browser.safari; + } else if (browser.vega) { + // UserAgent string contains 'Chrome' and 'Safari', but we only want 'vega' to be true + delete browser.chrome; + delete browser.safari; + // UserAgent string contains 'Mobile Chrome', but it is a TV + delete browser.mobile; } else { browser.orsay = normalizedUA.includes('smarthub'); } diff --git a/src/scripts/browser.test.ts b/src/scripts/browser.test.ts index 96b9fe8d54..a86f473618 100644 --- a/src/scripts/browser.test.ts +++ b/src/scripts/browser.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { detectBrowser } from './browser'; describe('Browser', () => { - it('should identify TitanOS devices', async () => { + it('should identify TitanOS devices', () => { // Ref: https://docs.titanos.tv/user-agents-specifications // Philips example let browser = detectBrowser('Mozilla/5.0 (Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.4147.62 Safari/537.36 OPR/46.0.2207.0 OMI/4.24, TV_NT72690_2025_4K / (Philips, , wired) CE-HTML/1.0 NETTV/4.6.0.8 SignOn/2.0 SmartTvA/5.0.0 TitanOS/3.0 en Ginga'); @@ -20,7 +20,17 @@ describe('Browser', () => { expect(browser.tv).toBe(true); }); - it('should identify Xbox devices', async () => { + it('should identify Vega devices', () => { + // Ref: https://developer.amazon.com/docs/vega/0.21/webview-development-best-practices-tv.html#avoid-relying-on-the-useragent + const browser = detectBrowser('Mozilla/5.0 (Linux; Kepler 1.1; AFTCA002 user/1234; wv) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Chrome/130.0.6723.192 Safari/537.36'); + expect(browser.vega).toBe(true); + expect(browser.chrome).toBeFalsy(); + expect(browser.safari).toBeFalsy(); + expect(browser.mobile).toBeFalsy(); + expect(browser.tv).toBe(true); + }); + + it('should identify Xbox devices', () => { const browser = detectBrowser('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 WebView2 Xbox'); expect(browser.xboxOne).toBe(true); expect(browser.tv).toBe(true); From 238c5bbf58ec38eff891312b92205712632a9f31 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 13 Jan 2026 13:39:01 -0500 Subject: [PATCH 05/10] Add vega os name and icon for dashboard --- src/assets/img/devices/firetv.svg | 1 + src/components/apphost.js | 1 + src/utils/image.ts | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 src/assets/img/devices/firetv.svg diff --git a/src/assets/img/devices/firetv.svg b/src/assets/img/devices/firetv.svg new file mode 100644 index 0000000000..b7348a3bd7 --- /dev/null +++ b/src/assets/img/devices/firetv.svg @@ -0,0 +1 @@ +Amazon Fire TV diff --git a/src/components/apphost.js b/src/components/apphost.js index ce88435a50..0693663f3c 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -13,6 +13,7 @@ const BrowserName = { tizen: 'Samsung Smart TV', web0s: 'LG Smart TV', titanos: 'Titan OS', + vega: 'Vega OS', operaTv: 'Opera TV', xboxOne: 'Xbox One', ps4: 'Sony PS4', diff --git a/src/utils/image.ts b/src/utils/image.ts index 60d10798e2..cf5edc60f6 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -33,6 +33,8 @@ function getWebDeviceIcon(browser: string | null | undefined) { return BASE_DEVICE_IMAGE_URL + 'msie.svg'; case 'Titan OS': return BASE_DEVICE_IMAGE_URL + 'titanos.svg'; + case 'Vega OS': + return BASE_DEVICE_IMAGE_URL + 'firetv.svg'; default: return BASE_DEVICE_IMAGE_URL + 'html5.svg'; } From 32d916b42061f7f2888656115491c82382288245 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 14 Jan 2026 17:35:15 -0500 Subject: [PATCH 06/10] Fix missing server id --- src/components/cardbuilder/cardBuilder.js | 3 ++- src/components/router/appRouter.js | 30 +++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 43298704db..a955f992dc 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -577,9 +577,10 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, if (flags.isOuterFooter && item.AlbumArtists?.length) { const artistText = item.AlbumArtists .map(artist => { + artist.ServerId = serverId; artist.Type = BaseItemKind.MusicArtist; artist.IsFolder = true; - return getTextActionButton(artist, null, serverId); + return getTextActionButton(artist); }) .join(' / '); lines.push(artistText); diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 032ddfaf1e..1c3bcb98dc 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -261,15 +261,15 @@ class AppRouter { } if (item === 'recordedtv') { - return '#/livetv?tab=3&serverId=' + options.serverId; + return '#/livetv?tab=3&serverId=' + serverId; } if (item === 'nextup') { - return '#/list?type=nextup&serverId=' + options.serverId; + return '#/list?type=nextup&serverId=' + serverId; } if (item === 'list') { - let urlForList = '#/list?serverId=' + options.serverId + '&type=' + options.itemTypes; + let urlForList = '#/list?serverId=' + serverId + '&type=' + options.itemTypes; if (options.isFavorite) { urlForList += '&IsFavorite=true'; @@ -304,49 +304,49 @@ class AppRouter { if (item === 'livetv') { if (options.section === 'programs') { - return '#/livetv?tab=0&serverId=' + options.serverId; + return '#/livetv?tab=0&serverId=' + serverId; } if (options.section === 'guide') { - return '#/livetv?tab=1&serverId=' + options.serverId; + return '#/livetv?tab=1&serverId=' + serverId; } if (options.section === 'movies') { - return '#/list?type=Programs&IsMovie=true&serverId=' + options.serverId; + return '#/list?type=Programs&IsMovie=true&serverId=' + serverId; } if (options.section === 'shows') { - return '#/list?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + options.serverId; + return '#/list?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + serverId; } if (options.section === 'sports') { - return '#/list?type=Programs&IsSports=true&serverId=' + options.serverId; + return '#/list?type=Programs&IsSports=true&serverId=' + serverId; } if (options.section === 'kids') { - return '#/list?type=Programs&IsKids=true&serverId=' + options.serverId; + return '#/list?type=Programs&IsKids=true&serverId=' + serverId; } if (options.section === 'news') { - return '#/list?type=Programs&IsNews=true&serverId=' + options.serverId; + return '#/list?type=Programs&IsNews=true&serverId=' + serverId; } if (options.section === 'onnow') { - return '#/list?type=Programs&IsAiring=true&serverId=' + options.serverId; + return '#/list?type=Programs&IsAiring=true&serverId=' + serverId; } if (options.section === 'channels') { - return '#/livetv?tab=2&serverId=' + options.serverId; + return '#/livetv?tab=2&serverId=' + serverId; } if (options.section === 'dvrschedule') { - return '#/livetv?tab=4&serverId=' + options.serverId; + return '#/livetv?tab=4&serverId=' + serverId; } if (options.section === 'seriesrecording') { - return '#/livetv?tab=5&serverId=' + options.serverId; + return '#/livetv?tab=5&serverId=' + serverId; } - return '#/livetv?serverId=' + options.serverId; + return '#/livetv?serverId=' + serverId; } if (itemType == 'SeriesTimer') { From a36eb7b5462259c6b39e2abcea71f50f1b9cc156 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 15 Jan 2026 18:59:15 -0500 Subject: [PATCH 07/10] Add sort options to movie collections --- src/apps/experimental/components/library/SortButton.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx index 6e8acf8b94..9842650270 100644 --- a/src/apps/experimental/components/library/SortButton.tsx +++ b/src/apps/experimental/components/library/SortButton.tsx @@ -22,6 +22,14 @@ type SortOption = { type SortOptionsMapping = Record; +const collectionMovieOptions: SortOption[] = [ + { label: 'Name', value: ItemSortBy.SortName }, + { label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, + { label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, + { label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, + { label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate } +]; + const movieOrFavoriteOptions = [ { label: 'Name', value: ItemSortBy.SortName }, { label: 'OptionRandom', value: ItemSortBy.Random }, @@ -43,6 +51,7 @@ const photosOrPhotoAlbumsOptions = [ const sortOptionsMapping: SortOptionsMapping = { [LibraryTab.Movies]: movieOrFavoriteOptions, + [LibraryTab.Collections]: collectionMovieOptions, [LibraryTab.Favorites]: movieOrFavoriteOptions, [LibraryTab.Series]: [ { label: 'Name', value: ItemSortBy.SortName }, From 2acc6f360a3781ae8eacaefade3ff72361d91e7f Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 16 Jan 2026 10:29:45 -0500 Subject: [PATCH 08/10] Add default font family for fallback --- src/styles/site.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/site.scss b/src/styles/site.scss index 3cb2d1fe74..53501e2b24 100644 --- a/src/styles/site.scss +++ b/src/styles/site.scss @@ -37,6 +37,9 @@ html { @include fullpage; + + /* Set the default font for cases we don't load the brand font */ + font-family: Arial, Helvetica, sans-serif; line-height: 1.35; } From bf31a733a73ee978ac2ee89aa322baf63875f32a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 16 Jan 2026 11:32:45 -0500 Subject: [PATCH 09/10] Restore back button in experimental layout for apps --- .../experimental/components/AppToolbar/index.tsx | 14 ++++++-------- src/components/router/appRouter.js | 6 ++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/apps/experimental/components/AppToolbar/index.tsx b/src/apps/experimental/components/AppToolbar/index.tsx index 308b96826a..b73b9b5bea 100644 --- a/src/apps/experimental/components/AppToolbar/index.tsx +++ b/src/apps/experimental/components/AppToolbar/index.tsx @@ -2,6 +2,7 @@ import Stack from '@mui/material/Stack'; import React, { type FC } from 'react'; import { useLocation } from 'react-router-dom'; +import { appRouter, PUBLIC_PATHS } from 'components/router/appRouter'; import AppToolbar from 'components/toolbar/AppToolbar'; import ServerButton from 'components/toolbar/ServerButton'; @@ -16,14 +17,6 @@ interface AppToolbarProps { onDrawerButtonClick: (event: React.MouseEvent) => void } -const PUBLIC_PATHS = [ - '/addserver', - '/selectserver', - '/login', - '/forgotpassword', - '/forgotpasswordpin' -]; - const ExperimentalAppToolbar: FC = ({ isDrawerAvailable, isDrawerOpen, @@ -34,6 +27,10 @@ const ExperimentalAppToolbar: FC = ({ // The video osd does not show the standard toolbar if (location.pathname === '/video') return null; + // Only show the back button in apps when appropriate + const isBackButtonAvailable = window.NativeShell && appRouter.canGoBack(location.pathname); + + // Check if the current path is a public path to hide user content const isPublicPath = PUBLIC_PATHS.includes(location.pathname); return ( @@ -48,6 +45,7 @@ const ExperimentalAppToolbar: FC = ({ isDrawerAvailable={isDrawerAvailable} isDrawerOpen={isDrawerOpen} onDrawerButtonClick={onDrawerButtonClick} + isBackButtonAvailable={isBackButtonAvailable} isUserMenuAvailable={!isPublicPath} > {!isDrawerAvailable && ( diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 032ddfaf1e..52964bdd4e 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -16,7 +16,7 @@ import { history } from 'RootAppRouter'; const START_PAGE_PATHS = ['/home', '/login', '/selectserver']; /** Pages that do not require a user to be logged in to view. */ -const PUBLIC_PATHS = [ +export const PUBLIC_PATHS = [ '/addserver', '/selectserver', '/login', @@ -121,9 +121,7 @@ class AppRouter { return this.baseRoute; } - canGoBack() { - const path = history.location.pathname; - + canGoBack(path = history.location.pathname) { if ( !document.querySelector('.dialogContainer') && START_PAGE_PATHS.includes(path) From ea2abad3e1671473d352b3ccf06f616c61ec9381 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 18 Jan 2026 20:03:02 -0500 Subject: [PATCH 10/10] Bump version to 10.11.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c8b47d9a2..33c2c98351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jellyfin-web", - "version": "10.11.5", + "version": "10.11.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "jellyfin-web", - "version": "10.11.5", + "version": "10.11.6", "license": "GPL-2.0-or-later", "dependencies": { "@emotion/react": "11.14.0", diff --git a/package.json b/package.json index c8c076ba7b..9b4fa138c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellyfin-web", - "version": "10.11.5", + "version": "10.11.6", "description": "Web interface for Jellyfin", "repository": "https://github.com/jellyfin/jellyfin-web", "license": "GPL-2.0-or-later",