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",
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
+
+
+
+ ))}
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
);
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/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',
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 },
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 @@
+
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/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..2d2903ca75 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)
@@ -261,15 +259,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 +302,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') {
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);
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;
}
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';
}