+ collectionType === CollectionType.Movies;
+
+export const isTVShows = (collectionType: string) =>
+ collectionType === CollectionType.Tvshows;
+
+export const isMusic = (collectionType: string) =>
+ collectionType === CollectionType.Music;
+
+export const isLivetv = (collectionType: string) =>
+ collectionType === CollectionType.Livetv;
+
+export function addSection(
+ sections: Section[],
+ title: string,
+ items: BaseItemDto[] | null | undefined,
+ cardOptions?: CardOptions
+) {
+ if (items && items?.length > 0) {
+ sections.push({ title, items, cardOptions });
+ }
+}
+
+export function sortSections(sections: Section[]) {
+ return sections.sort((a, b) => {
+ const indexA = SEARCH_SECTIONS_SORT_ORDER.indexOf(a.title);
+ const indexB = SEARCH_SECTIONS_SORT_ORDER.indexOf(b.title);
+
+ if (indexA > indexB) {
+ return 1;
+ } else if (indexA < indexB) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+}
+
+export function getCardOptionsFromType(type: BaseItemKind) {
+ switch (type) {
+ case BaseItemKind.Movie:
+ case BaseItemKind.Series:
+ case BaseItemKind.MusicAlbum:
+ return {
+ showYear: true
+ };
+ case BaseItemKind.Episode:
+ return {
+ coverImage: true,
+ showParentTitle: true
+ };
+ case BaseItemKind.MusicArtist:
+ return {
+ coverImage: true
+ };
+ case BaseItemKind.Audio:
+ return {
+ showParentTitle: true,
+ shape: CardShape.SquareOverflow
+ };
+ case BaseItemKind.LiveTvProgram:
+ return LIVETV_CARD_OPTIONS;
+ default:
+ return {};
+ }
+}
+
+export function getTitleFromType(type: BaseItemKind) {
+ switch (type) {
+ case BaseItemKind.Movie:
+ return 'Movies';
+ case BaseItemKind.Series:
+ return 'Shows';
+ case BaseItemKind.Episode:
+ return 'Episodes';
+ case BaseItemKind.Playlist:
+ return 'Playlists';
+ case BaseItemKind.MusicAlbum:
+ return 'Albums';
+ case BaseItemKind.Audio:
+ return 'Songs';
+ case BaseItemKind.LiveTvProgram:
+ return 'Programs';
+ case BaseItemKind.TvChannel:
+ return 'Channels';
+ case BaseItemKind.PhotoAlbum:
+ return 'HeaderPhotoAlbums';
+ case BaseItemKind.Photo:
+ return 'Photos';
+ case BaseItemKind.AudioBook:
+ return 'HeaderAudioBooks';
+ case BaseItemKind.Book:
+ return 'Books';
+ case BaseItemKind.BoxSet:
+ return 'Collections';
+ default:
+ return '';
+ }
+}
+
+export function getItemTypesFromCollectionType(collectionType: CollectionType | undefined) {
+ switch (collectionType) {
+ case CollectionType.Movies:
+ return [ BaseItemKind.Movie ];
+ case CollectionType.Tvshows:
+ return [
+ BaseItemKind.Series,
+ BaseItemKind.Episode
+ ];
+ case CollectionType.Music:
+ return [
+ BaseItemKind.Playlist,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.Audio
+ ];
+ default:
+ return [
+ BaseItemKind.Movie,
+ BaseItemKind.Series,
+ BaseItemKind.Episode,
+ BaseItemKind.Playlist,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.Audio,
+ BaseItemKind.TvChannel,
+ BaseItemKind.PhotoAlbum,
+ BaseItemKind.Photo,
+ BaseItemKind.AudioBook,
+ BaseItemKind.Book,
+ BaseItemKind.BoxSet
+ ];
+ }
+}
diff --git a/src/apps/stable/routes/search.tsx b/src/apps/stable/routes/search.tsx
index 290ea60406..f06ce927d8 100644
--- a/src/apps/stable/routes/search.tsx
+++ b/src/apps/stable/routes/search.tsx
@@ -4,9 +4,10 @@ import { useDebounceValue } from 'usehooks-ts';
import { usePrevious } from 'hooks/usePrevious';
import globalize from 'lib/globalize';
import Page from 'components/Page';
-import SearchFields from 'components/search/SearchFields';
-import SearchSuggestions from 'components/search/SearchSuggestions';
-import SearchResults from 'components/search/SearchResults';
+import SearchFields from 'apps/stable/features/search/components/SearchFields';
+import SearchSuggestions from 'apps/stable/features/search/components/SearchSuggestions';
+import SearchResults from 'apps/stable/features/search/components/SearchResults';
+import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
const COLLECTION_TYPE_PARAM = 'collectionType';
const PARENT_ID_PARAM = 'parentId';
@@ -15,7 +16,7 @@ const QUERY_PARAM = 'query';
const Search: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
- const collectionTypeQuery = searchParams.get(COLLECTION_TYPE_PARAM) || undefined;
+ const collectionTypeQuery = (searchParams.get(COLLECTION_TYPE_PARAM) || undefined) as CollectionType | undefined;
const urlQuery = searchParams.get(QUERY_PARAM) || '';
const [query, setQuery] = useState(urlQuery);
const prevQuery = usePrevious(query, '');
@@ -50,7 +51,7 @@ const Search: FC = () => {
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
>
- {!query ? (
+ {!debouncedQuery ? (
diff --git a/src/components/CustomCss.tsx b/src/components/CustomCss.tsx
new file mode 100644
index 0000000000..48c684724d
--- /dev/null
+++ b/src/components/CustomCss.tsx
@@ -0,0 +1,26 @@
+import React, { type FC } from 'react';
+
+import { useUserSettings } from 'hooks/useUserSettings';
+import { useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
+
+const CustomCss: FC = () => {
+ const { data: brandingOptions } = useBrandingOptions();
+ const { customCss: userCustomCss, disableCustomCss } = useUserSettings();
+
+ return (
+ <>
+ {!disableCustomCss && brandingOptions?.CustomCss && (
+
+ )}
+ {userCustomCss && (
+
+ )}
+ >
+ );
+};
+
+export default CustomCss;
diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js
index 9920b9baae..38692998f2 100644
--- a/src/components/ServerConnections.js
+++ b/src/components/ServerConnections.js
@@ -1,5 +1,5 @@
// NOTE: This is used for jsdoc return type
-// eslint-disable-next-line no-unused-vars
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Api } from '@jellyfin/sdk';
import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions';
import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient';
@@ -18,7 +18,6 @@ const normalizeImageOptions = options => {
};
const getMaxBandwidth = () => {
- /* eslint-disable compat/compat */
if (navigator.connection) {
let max = navigator.connection.downlinkMax;
if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
@@ -28,7 +27,6 @@ const getMaxBandwidth = () => {
return parseInt(max, 10);
}
}
- /* eslint-enable compat/compat */
return null;
};
diff --git a/src/components/ThemeCss.tsx b/src/components/ThemeCss.tsx
new file mode 100644
index 0000000000..9dabeee15a
--- /dev/null
+++ b/src/components/ThemeCss.tsx
@@ -0,0 +1,34 @@
+import React, { type FC, useEffect, useState } from 'react';
+
+import { useUserTheme } from 'hooks/useUserTheme';
+import { getDefaultTheme } from 'scripts/settings/webSettings';
+
+interface ThemeCssProps {
+ dashboard?: boolean
+}
+
+const getThemeUrl = (id: string) => `themes/${id}/theme.css`;;
+
+const DEFAULT_THEME_URL = getThemeUrl(getDefaultTheme().id);
+
+const ThemeCss: FC
= ({
+ dashboard = false
+}) => {
+ const { theme, dashboardTheme } = useUserTheme();
+ const [ themeUrl, setThemeUrl ] = useState(DEFAULT_THEME_URL);
+
+ useEffect(() => {
+ const id = dashboard ? dashboardTheme : theme;
+ if (id) setThemeUrl(getThemeUrl(id));
+ }, [dashboard, dashboardTheme, theme]);
+
+ return (
+
+ );
+};
+
+export default ThemeCss;
diff --git a/src/components/apphost.js b/src/components/apphost.js
index 984c3a77bf..7d49088564 100644
--- a/src/components/apphost.js
+++ b/src/components/apphost.js
@@ -452,7 +452,7 @@ let isHidden = false;
let hidden;
let visibilityChange;
-if (typeof document.hidden !== 'undefined') { /* eslint-disable-line compat/compat */
+if (typeof document.hidden !== 'undefined') {
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
@@ -461,7 +461,6 @@ if (typeof document.hidden !== 'undefined') { /* eslint-disable-line compat/comp
}
document.addEventListener(visibilityChange, function () {
- /* eslint-disable-next-line compat/compat */
if (document[hidden]) {
onAppHidden();
} else {
diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index 54e9d33a8e..1f7da16557 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -484,7 +484,7 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) {
airTimeText += ' - ' + datetime.getDisplayTime(date);
}
} catch (e) {
- console.error('error parsing date: ' + item.StartDate);
+ console.error('error parsing date: ' + item.StartDate, e);
}
}
@@ -617,7 +617,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
datetime.parseISO8601Date(item.PremiereDate),
{ weekday: 'long', month: 'long', day: 'numeric' }
));
- } catch (err) {
+ } catch {
lines.push('');
}
} else {
diff --git a/src/components/cardbuilder/cardBuilderUtils.test.ts b/src/components/cardbuilder/cardBuilderUtils.test.ts
index 7aa749af12..af5d189f36 100644
--- a/src/components/cardbuilder/cardBuilderUtils.test.ts
+++ b/src/components/cardbuilder/cardBuilderUtils.test.ts
@@ -678,6 +678,7 @@ describe('getDefaultBackgroundClass', () => {
});
test('randomization string provided', () => {
+ // eslint-disable-next-line sonarjs/pseudo-random
const generateRandomString = (stringLength: number): string => (Math.random() + 1).toString(36).substring(stringLength);
for (let i = 0; i < 100; i++) {
diff --git a/src/components/filterdialog/filterdialog.js b/src/components/filterdialog/filterdialog.js
index 3eacd7db82..408ef06de6 100644
--- a/src/components/filterdialog/filterdialog.js
+++ b/src/components/filterdialog/filterdialog.js
@@ -14,6 +14,7 @@ function merge(resultItems, queryItems, delimiter) {
if (!queryItems) {
return resultItems;
}
+ // eslint-disable-next-line sonarjs/no-alphabetical-sort
return union(resultItems, queryItems.split(delimiter)).sort();
}
diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js
index a56ae355f4..0b72fc023d 100644
--- a/src/components/guide/guide.js
+++ b/src/components/guide/guide.js
@@ -358,7 +358,7 @@ function Guide(options) {
if ((typeof date).toString().toLowerCase() === 'string') {
try {
date = datetime.parseISO8601Date(date, { toLocal: true });
- } catch (err) {
+ } catch {
return date;
}
}
@@ -392,7 +392,7 @@ function Guide(options) {
try {
program.StartDateLocal = datetime.parseISO8601Date(program.StartDate, { toLocal: true });
} catch (err) {
- console.error('error parsing timestamp for start date');
+ console.error('error parsing timestamp for start date', err);
}
}
@@ -400,7 +400,7 @@ function Guide(options) {
try {
program.EndDateLocal = datetime.parseISO8601Date(program.EndDate, { toLocal: true });
} catch (err) {
- console.error('error parsing timestamp for end date');
+ console.error('error parsing timestamp for end date', err);
}
}
diff --git a/src/components/imageUploader/imageUploader.js b/src/components/imageUploader/imageUploader.js
index ff07de1553..cc211fde9b 100644
--- a/src/components/imageUploader/imageUploader.js
+++ b/src/components/imageUploader/imageUploader.js
@@ -77,6 +77,7 @@ function setFiles(page, files) {
reader.readAsDataURL(file);
}
+// eslint-disable-next-line sonarjs/no-invariant-returns
function onSubmit(e) {
const file = currentFile;
diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js
index 561c968623..c41b0d59ff 100644
--- a/src/components/images/imageLoader.js
+++ b/src/components/images/imageLoader.js
@@ -2,7 +2,7 @@ import Worker from './blurhash.worker.ts'; // eslint-disable-line import/default
import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
import * as userSettings from '../../scripts/settings/userSettings';
import './style.scss';
-// eslint-disable-next-line compat/compat
+
const worker = new Worker();
const targetDic = {};
worker.addEventListener(
diff --git a/src/components/listview/List/Lists.tsx b/src/components/listview/List/Lists.tsx
index 1215851cc5..3a45be39c1 100644
--- a/src/components/listview/List/Lists.tsx
+++ b/src/components/listview/List/Lists.tsx
@@ -25,7 +25,6 @@ const Lists: FC = ({ items = [], listOptions = {} }) => {
const renderListItem = (item: ItemDto, index: number) => {
return (
{
- const response = await getItemsApi(api).getItems(
- {
- ...QUERY_OPTIONS,
- userId: userId,
- recursive: true,
- ...params
- },
- options
- );
- return response.data;
-};
-
-const fetchPeople = async (
- api: Api,
- userId: string,
- params?: PersonsApiGetPersonsRequest,
- options?: AxiosRequestConfig
-) => {
- const response = await getPersonsApi(api).getPersons(
- {
- ...QUERY_OPTIONS,
- userId: userId,
- ...params
- },
- options
- );
- return response.data;
-};
-
-const fetchArtists = async (
- api: Api,
- userId: string,
- params?: ArtistsApiGetArtistsRequest,
- options?: AxiosRequestConfig
-) => {
- const response = await getArtistsApi(api).getArtists(
- {
- ...QUERY_OPTIONS,
- userId: userId,
- ...params
- },
- options
- );
- return response.data;
-};
-
-const isMovies = (collectionType: string) =>
- collectionType === CollectionType.Movies;
-
-const isMusic = (collectionType: string) =>
- collectionType === CollectionType.Music;
-
-const isTVShows = (collectionType: string) =>
- collectionType === CollectionType.Tvshows;
-
-const isLivetv = (collectionType: string) =>
- collectionType === CollectionType.Livetv;
-
-const LIVETV_CARD_OPTIONS = {
- preferThumb: true,
- inheritThumb: false,
- showParentTitleOrTitle: true,
- showTitle: false,
- coverImage: true,
- overlayMoreButton: true,
- showAirTime: true,
- showAirDateTime: true,
- showChannelName: true
-};
-
-export interface Section {
- title: string
- items: BaseItemDto[];
- cardOptions?: CardOptions;
-}
-
-export const useSearchItems = (
- parentId?: string,
- collectionType?: string,
- searchTerm?: string
-) => {
- const { api, user } = useApi();
- const userId = user?.Id;
-
- return useQuery({
- queryKey: ['SearchItems', { parentId, collectionType, searchTerm }],
- queryFn: async ({ signal }) => {
- if (!api) throw new Error('No API instance available');
- if (!userId) throw new Error('No User ID provided');
-
- const sections: Section[] = [];
-
- const addSection = (
- title: string,
- items: BaseItemDto[] | null | undefined,
- cardOptions?: CardOptions
- ) => {
- if (items && items?.length > 0) {
- sections.push({ title, items, cardOptions });
- }
- };
-
- // Livetv libraries
- if (collectionType && isLivetv(collectionType)) {
- // Movies row
- const moviesData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isMovie: true,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Movies', moviesData.Items, {
- ...LIVETV_CARD_OPTIONS,
- shape: CardShape.PortraitOverflow
- });
-
- // Episodes row
- const episodesData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isMovie: false,
- isSeries: true,
- isSports: false,
- isKids: false,
- isNews: false,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Episodes', episodesData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // Sports row
- const sportsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isSports: true,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Sports', sportsData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // Kids row
- const kidsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isKids: true,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Kids', kidsData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // News row
- const newsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isNews: true,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('News', newsData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // Programs row
- const programsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- isMovie: false,
- isSeries: false,
- isSports: false,
- isKids: false,
- isNews: false,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Programs', programsData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // Channels row
- const channelsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.TvChannel],
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Channels', channelsData.Items);
- }
-
- // Movie libraries
- if (!collectionType || isMovies(collectionType)) {
- // Movies row
- const moviesData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Movie],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Movies', moviesData.Items, {
- showYear: true
- });
- }
-
- // TV Show libraries
- if (!collectionType || isTVShows(collectionType)) {
- // Shows row
- const showsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Series],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Shows', showsData.Items, {
- showYear: true
- });
-
- // Episodes row
- const episodesData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Episode],
- parentId: parentId,
- isMissing: user?.Configuration?.DisplayMissingEpisodes,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Episodes', episodesData.Items, {
- coverImage: true,
- showParentTitle: true
- });
- }
-
- // People are included for Movies and TV Shows
- if (
- !collectionType
- || isMovies(collectionType)
- || isTVShows(collectionType)
- ) {
- // People row
- const peopleData = await fetchPeople(
- api,
- userId,
- {
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('People', peopleData.Items, {
- coverImage: true
- });
- }
-
- // Music libraries
- if (!collectionType || isMusic(collectionType)) {
- // Playlists row
- const playlistsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Playlist],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Playlists', playlistsData.Items);
-
- // Artists row
- const artistsData = await fetchArtists(
- api,
- userId,
- {
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Artists', artistsData.Items, {
- coverImage: true
- });
-
- // Albums row
- const albumsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.MusicAlbum],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Albums', albumsData.Items, {
- showYear: true
- });
-
- // Songs row
- const songsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Audio],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Songs', songsData.Items, {
- showParentTitle: true,
- shape: CardShape.SquareOverflow
- });
- }
-
- // Other libraries do not support in-library search currently
- if (!collectionType) {
- // Videos row
- const videosData = await fetchItemsByType(
- api,
- userId,
- {
- mediaTypes: [MediaType.Video],
- excludeItemTypes: [
- BaseItemKind.Movie,
- BaseItemKind.Episode,
- BaseItemKind.TvChannel
- ],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
-
- addSection('HeaderVideos', videosData.Items, {
- showParentTitle: true
- });
-
- // Programs row
- const programsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.LiveTvProgram],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Programs', programsData.Items, {
- ...LIVETV_CARD_OPTIONS
- });
-
- // Channels row
- const channelsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.TvChannel],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Channels', channelsData.Items);
-
- // Photo Albums row
- const photoAlbumsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.PhotoAlbum],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('HeaderPhotoAlbums', photoAlbumsData.Items);
-
- // Photos row
- const photosData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Photo],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Photos', photosData.Items);
-
- // Audio Books row
- const audioBooksData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.AudioBook],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('HeaderAudioBooks', audioBooksData.Items);
-
- // Books row
- const booksData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.Book],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Books', booksData.Items);
-
- // Collections row
- const collectionsData = await fetchItemsByType(
- api,
- userId,
- {
- includeItemTypes: [BaseItemKind.BoxSet],
- parentId: parentId,
- searchTerm: searchTerm
- },
- { signal }
- );
- addSection('Collections', collectionsData.Items);
- }
-
- return sections;
- },
- enabled: !!api && !!userId
- });
-};
diff --git a/src/hooks/useUserSettings.tsx b/src/hooks/useUserSettings.tsx
index 84eb49117e..289588669e 100644
--- a/src/hooks/useUserSettings.tsx
+++ b/src/hooks/useUserSettings.tsx
@@ -7,6 +7,8 @@ import Events, { type Event } from 'utils/events';
import { useApi } from './useApi';
interface UserSettings {
+ customCss?: string
+ disableCustomCss: boolean
theme?: string
dashboardTheme?: string
dateTimeLocale?: string
@@ -15,6 +17,9 @@ interface UserSettings {
// NOTE: This is an incomplete list of only the settings that are currently being used
const UserSettingField = {
+ // Custom CSS
+ CustomCss: 'customCss',
+ DisableCustomCss: 'disableCustomCss',
// Theme settings
Theme: 'appTheme',
DashboardTheme: 'dashboardTheme',
@@ -23,11 +28,15 @@ const UserSettingField = {
Language: 'language'
};
-const UserSettingsContext = createContext({});
+const UserSettingsContext = createContext({
+ disableCustomCss: false
+});
export const useUserSettings = () => useContext(UserSettingsContext);
export const UserSettingsProvider: FC> = ({ children }) => {
+ const [ customCss, setCustomCss ] = useState();
+ const [ disableCustomCss, setDisableCustomCss ] = useState(false);
const [ theme, setTheme ] = useState();
const [ dashboardTheme, setDashboardTheme ] = useState();
const [ dateTimeLocale, setDateTimeLocale ] = useState();
@@ -36,14 +45,25 @@ export const UserSettingsProvider: FC> = ({ children
const { user } = useApi();
const context = useMemo(() => ({
+ customCss,
+ disableCustomCss,
theme,
dashboardTheme,
dateTimeLocale,
locale: language
- }), [ theme, dashboardTheme, dateTimeLocale, language ]);
+ }), [
+ customCss,
+ disableCustomCss,
+ theme,
+ dashboardTheme,
+ dateTimeLocale,
+ language
+ ]);
// Update the values of the user settings
const updateUserSettings = useCallback(() => {
+ setCustomCss(userSettings.customCss());
+ setDisableCustomCss(userSettings.disableCustomCss());
setTheme(userSettings.theme());
setDashboardTheme(userSettings.dashboardTheme());
setDateTimeLocale(userSettings.dateTimeLocale());
diff --git a/src/index.jsx b/src/index.jsx
index 169f44857c..81627c4ad8 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -17,7 +17,6 @@ import { loadCoreDictionary } from 'lib/globalize/loader';
import { initialize as initializeAutoCast } from 'scripts/autocast';
import browser from './scripts/browser';
import keyboardNavigation from './scripts/keyboardNavigation';
-import { currentSettings } from './scripts/settings/userSettings';
import { getPlugins } from './scripts/settings/webSettings';
import taskButton from './scripts/taskbutton';
import { pageClassOn, serverAddress } from './utils/dashboard';
@@ -116,9 +115,6 @@ build: ${__JF_BUILD_VERSION__}`);
// Load platform specific features
loadPlatformFeatures();
- // Load custom CSS styles
- loadCustomCss();
-
// Enable navigation controls
keyboardNavigation.enable();
autoFocuser.enable();
@@ -191,54 +187,7 @@ function loadPlatformFeatures() {
}
}
-function loadCustomCss() {
- // Apply custom CSS
- const apiClient = ServerConnections.currentApiClient();
- if (apiClient) {
- const brandingCss = fetch(apiClient.getUrl('Branding/Css'))
- .then(function(response) {
- if (!response.ok) {
- throw new Error(response.status + ' ' + response.statusText);
- }
- return response.text();
- })
- .catch(function(err) {
- console.warn('Error applying custom css', err);
- });
-
- const handleStyleChange = async () => {
- let style = document.querySelector('#cssBranding');
- if (!style) {
- // Inject the branding css as a dom element in body so it will take
- // precedence over other stylesheets
- style = document.createElement('style');
- style.id = 'cssBranding';
- document.body.appendChild(style);
- }
-
- const css = [];
- // Only add branding CSS when enabled
- if (!currentSettings.disableCustomCss()) css.push(await brandingCss);
- // Always add user CSS
- css.push(currentSettings.customCss());
-
- style.textContent = css.join('\n');
- };
-
- Events.on(ServerConnections, 'localusersignedin', handleStyleChange);
- Events.on(ServerConnections, 'localusersignedout', handleStyleChange);
- Events.on(currentSettings, 'change', (e, prop) => {
- if (prop == 'disableCustomCss' || prop == 'customCss') {
- handleStyleChange();
- }
- });
-
- handleStyleChange();
- }
-}
-
function registerServiceWorker() {
- /* eslint-disable compat/compat */
if (navigator.serviceWorker && window.appMode !== 'cordova' && window.appMode !== 'android') {
navigator.serviceWorker.register('serviceworker.js').then(() =>
console.log('serviceWorker registered')
@@ -248,7 +197,6 @@ function registerServiceWorker() {
} else {
console.warn('serviceWorker unsupported');
}
- /* eslint-enable compat/compat */
}
async function renderApp() {
diff --git a/src/lib/globalize/index.js b/src/lib/globalize/index.js
index fc76a209df..1029a2280d 100644
--- a/src/lib/globalize/index.js
+++ b/src/lib/globalize/index.js
@@ -79,7 +79,7 @@ export function updateCurrentCulture() {
let culture;
try {
culture = userSettings.language();
- } catch (err) {
+ } catch {
console.error('no language set in user settings');
}
culture = culture || getDefaultLanguage();
@@ -92,7 +92,7 @@ export function updateCurrentCulture() {
let dateTimeCulture;
try {
dateTimeCulture = userSettings.dateTimeLocale();
- } catch (err) {
+ } catch {
console.error('no date format set in user settings');
}
diff --git a/src/lib/legacy/domParserTextHtml.js b/src/lib/legacy/domParserTextHtml.js
index cea84c79f6..9cace5df2e 100644
--- a/src/lib/legacy/domParserTextHtml.js
+++ b/src/lib/legacy/domParserTextHtml.js
@@ -22,7 +22,7 @@
// text/html parsing is natively supported
return;
}
- } catch (ex) { /* noop */ }
+ } catch { /* noop */ }
DOMParserPrototype.parseFromString = function (markup, type) {
if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
diff --git a/src/lib/legacy/keyboardEvent.js b/src/lib/legacy/keyboardEvent.js
index b2e114a383..488a914a19 100644
--- a/src/lib/legacy/keyboardEvent.js
+++ b/src/lib/legacy/keyboardEvent.js
@@ -8,7 +8,7 @@
try {
new window.KeyboardEvent('event', { bubbles: true, cancelable: true });
- } catch (e) {
+ } catch {
// We can't use `KeyboardEvent` in old WebKit because `initKeyboardEvent`
// doesn't seem to populate some properties (`keyCode`, `which`) that
// are read-only.
diff --git a/src/lib/legacy/patchHeaders.js b/src/lib/legacy/patchHeaders.js
index 5c99b6a358..e8d7c80922 100644
--- a/src/lib/legacy/patchHeaders.js
+++ b/src/lib/legacy/patchHeaders.js
@@ -13,7 +13,7 @@
if (window.Headers) {
try {
new window.Headers(undefined);
- } catch (_) {
+ } catch {
console.debug('patch \'Headers\' to accept \'undefined\'');
const _Headers = window.Headers;
diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js
index 59b01e15c2..868f026918 100644
--- a/src/plugins/chromecastPlayer/plugin.js
+++ b/src/plugins/chromecastPlayer/plugin.js
@@ -1109,7 +1109,8 @@ class ChromecastPlayer {
return this.getPlayerStateInternal()?.NowPlayingItem?.IndexNumber;
}
- clearQueue(currentTime) { // eslint-disable-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ clearQueue(currentTime) {
// not supported yet
}
}
diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js
index f25ba3994e..9ab641e9e7 100644
--- a/src/plugins/htmlVideoPlayer/plugin.js
+++ b/src/plugins/htmlVideoPlayer/plugin.js
@@ -1512,7 +1512,7 @@ export class HtmlVideoPlayer {
trackElement.removeCue(trackElement.cues[0]);
}
} catch (e) {
- console.error('error removing cue from textTrack');
+ console.error('error removing cue from textTrack', e);
}
trackElement.mode = 'disabled';
diff --git a/src/plugins/syncPlay/core/PlaybackCore.js b/src/plugins/syncPlay/core/PlaybackCore.js
index 73ab977eb6..c148d30b29 100644
--- a/src/plugins/syncPlay/core/PlaybackCore.js
+++ b/src/plugins/syncPlay/core/PlaybackCore.js
@@ -221,6 +221,7 @@ class PlaybackCore {
// Account for player imperfections, we got half a second of tollerance we can play with
// (the server tollerates a range of values when client reports that is ready).
const rangeWidth = 100; // In milliseconds.
+ // eslint-disable-next-line sonarjs/pseudo-random
const randomOffsetTicks = Math.round((Math.random() - 0.5) * rangeWidth) * Helper.TicksPerMillisecond;
this.scheduleSeek(command.When, command.PositionTicks + randomOffsetTicks);
console.debug('SyncPlay applyCommand: adding random offset to force seek:', randomOffsetTicks, command);
diff --git a/src/plugins/syncPlay/core/players/GenericPlayer.js b/src/plugins/syncPlay/core/players/GenericPlayer.js
index 2093191e62..ff127b1a03 100644
--- a/src/plugins/syncPlay/core/players/GenericPlayer.js
+++ b/src/plugins/syncPlay/core/players/GenericPlayer.js
@@ -158,7 +158,7 @@ class GenericPlayer {
* Sets the playback rate, if supported.
* @param {number} value The playback rate.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
setPlaybackRate(value) {
// Do nothing.
}
@@ -197,7 +197,7 @@ class GenericPlayer {
* Seeks the player to the specified position.
* @param {number} positionTicks The new position.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSeek(positionTicks) {
// Override
}
@@ -213,7 +213,7 @@ class GenericPlayer {
* Sends a command to the player.
* @param {Object} command The command.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSendCommand(command) {
// Override
}
@@ -222,7 +222,7 @@ class GenericPlayer {
* Starts playback.
* @param {Object} options Playback data.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localPlay(options) {
// Override
}
@@ -231,7 +231,7 @@ class GenericPlayer {
* Sets playing item from playlist.
* @param {string} playlistItemId The item to play.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSetCurrentPlaylistItem(playlistItemId) {
// Override
}
@@ -240,7 +240,7 @@ class GenericPlayer {
* Removes items from playlist.
* @param {Array} playlistItemIds The items to remove.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localRemoveFromPlaylist(playlistItemIds) {
// Override
}
@@ -250,7 +250,7 @@ class GenericPlayer {
* @param {string} playlistItemId The item to move.
* @param {number} newIndex The new position.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localMovePlaylistItem(playlistItemId, newIndex) {
// Override
}
@@ -259,7 +259,7 @@ class GenericPlayer {
* Queues in the playlist.
* @param {Object} options Queue data.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localQueue(options) {
// Override
}
@@ -268,7 +268,7 @@ class GenericPlayer {
* Queues after the playing item in the playlist.
* @param {Object} options Queue data.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localQueueNext(options) {
// Override
}
@@ -291,7 +291,7 @@ class GenericPlayer {
* Sets repeat mode.
* @param {string} value The repeat mode.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSetRepeatMode(value) {
// Override
}
@@ -300,7 +300,7 @@ class GenericPlayer {
* Sets shuffle mode.
* @param {string} value The shuffle mode.
*/
- // eslint-disable-next-line no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSetQueueShuffleMode(value) {
// Override
}
diff --git a/src/scripts/browserDeviceProfile.js b/src/scripts/browserDeviceProfile.js
index 4370edaeaf..f38d7ab6e7 100644
--- a/src/scripts/browserDeviceProfile.js
+++ b/src/scripts/browserDeviceProfile.js
@@ -913,6 +913,19 @@ export default function (options) {
profile.ContainerProfiles = [];
+ if (browser.tizen) {
+ // Tizen doesn't support more than 32 streams in a single file
+ profile.ContainerProfiles.push({
+ Type: 'Video',
+ Conditions: [{
+ Condition: 'LessThanEqual',
+ Property: 'NumStreams',
+ Value: '32',
+ IsRequired: false
+ }]
+ });
+ }
+
profile.CodecProfiles = [];
const supportsSecondaryAudio = canPlaySecondaryAudio(videoTestElement);
diff --git a/src/scripts/datetime.js b/src/scripts/datetime.js
index 6de53c0167..bcc48b1d74 100644
--- a/src/scripts/datetime.js
+++ b/src/scripts/datetime.js
@@ -211,7 +211,7 @@ export function getDisplayDateTime(date) {
if (typeof date === 'string') {
try {
date = parseISO8601Date(date, true);
- } catch (err) {
+ } catch {
return date;
}
}
@@ -227,7 +227,7 @@ export function getDisplayTime(date) {
if (typeof date === 'string') {
try {
date = parseISO8601Date(date, true);
- } catch (err) {
+ } catch {
return date;
}
}
diff --git a/src/scripts/gamepadtokey.js b/src/scripts/gamepadtokey.js
index da3b88e27f..571c06c32c 100644
--- a/src/scripts/gamepadtokey.js
+++ b/src/scripts/gamepadtokey.js
@@ -178,7 +178,7 @@ function resetThrottle(key) {
const isElectron = navigator.userAgent.toLowerCase().indexOf('electron') !== -1;
function allowInput() {
// This would be nice but always seems to return true with electron
- if (!isElectron && document.hidden) { /* eslint-disable-line compat/compat */
+ if (!isElectron && document.hidden) {
return false;
}
@@ -356,7 +356,6 @@ function isGamepadConnected() {
}
function onFocusOrGamepadAttach() {
- /* eslint-disable-next-line compat/compat */
if (isGamepadConnected() && document.hasFocus()) {
console.log('Gamepad connected! Starting input loop');
startInputLoop();
@@ -364,7 +363,6 @@ function onFocusOrGamepadAttach() {
}
function onFocusOrGamepadDetach() {
- /* eslint-disable-next-line compat/compat */
if (!isGamepadConnected() || !document.hasFocus()) {
console.log('Gamepad disconnected! No other gamepads are connected, stopping input loop');
stopInputLoop();
diff --git a/src/scripts/keyboardNavigation.js b/src/scripts/keyboardNavigation.js
index 8e5797cd8d..a07a8a270e 100644
--- a/src/scripts/keyboardNavigation.js
+++ b/src/scripts/keyboardNavigation.js
@@ -64,7 +64,7 @@ let hasFieldKey = false;
try {
hasFieldKey = 'key' in new KeyboardEvent('keydown');
} catch (e) {
- console.error("error checking 'key' field");
+ console.error("error checking 'key' field", e);
}
if (!hasFieldKey) {
@@ -239,7 +239,7 @@ function attachGamepadScript() {
}
// No need to check for gamepads manually at load time, the eventhandler will be fired for that
-if (navigator.getGamepads && appSettings.enableGamepad()) { /* eslint-disable-line compat/compat */
+if (navigator.getGamepads && appSettings.enableGamepad()) {
window.addEventListener('gamepadconnected', attachGamepadScript);
}
diff --git a/src/scripts/screensavermanager.js b/src/scripts/screensavermanager.js
index 2ef416a387..fccc2751a8 100644
--- a/src/scripts/screensavermanager.js
+++ b/src/scripts/screensavermanager.js
@@ -31,7 +31,7 @@ function getScreensaverPlugin(isLoggedIn) {
let option;
try {
option = userSettings.get('screensaver', false);
- } catch (err) {
+ } catch {
option = isLoggedIn ? 'backdropscreensaver' : 'logoscreensaver';
}
diff --git a/src/scripts/themeManager.js b/src/scripts/themeManager.js
index 70f4810041..f976dba82e 100644
--- a/src/scripts/themeManager.js
+++ b/src/scripts/themeManager.js
@@ -1,16 +1,7 @@
import { getDefaultTheme, getThemes as getConfiguredThemes } from './settings/webSettings';
-let themeStyleElement = document.querySelector('#cssTheme');
let currentThemeId;
-function unloadTheme() {
- const elem = themeStyleElement;
- if (elem) {
- elem.removeAttribute('href');
- currentThemeId = null;
- }
-}
-
function getThemes() {
return getConfiguredThemes();
}
@@ -29,11 +20,7 @@ function getThemeStylesheetInfo(id) {
theme = getDefaultTheme();
}
- return {
- stylesheetPath: 'themes/' + theme.id + '/theme.css',
- themeId: theme.id,
- color: theme.color
- };
+ return theme;
});
}
@@ -45,36 +32,12 @@ function setTheme(id) {
}
getThemeStylesheetInfo(id).then(function (info) {
- if (currentThemeId && currentThemeId === info.themeId) {
+ if (currentThemeId && currentThemeId === info.id) {
resolve();
return;
}
- const linkUrl = info.stylesheetPath;
- unloadTheme();
-
- let link = themeStyleElement;
-
- if (!link) {
- // Inject the theme css as a dom element in body so it will take
- // precedence over other stylesheets
- link = document.createElement('link');
- link.id = 'cssTheme';
- link.setAttribute('rel', 'stylesheet');
- link.setAttribute('type', 'text/css');
- document.body.appendChild(link);
- }
-
- const onLoad = function (e) {
- e.target.removeEventListener('load', onLoad);
- resolve();
- };
-
- link.addEventListener('load', onLoad);
-
- link.setAttribute('href', linkUrl);
- themeStyleElement = link;
- currentThemeId = info.themeId;
+ currentThemeId = info.id;
document.getElementById('themeColor').content = info.color;
});
@@ -82,6 +45,6 @@ function setTheme(id) {
}
export default {
- getThemes: getThemes,
- setTheme: setTheme
+ getThemes,
+ setTheme
};
diff --git a/src/strings/ar.json b/src/strings/ar.json
index 3dc810e181..065672e63f 100644
--- a/src/strings/ar.json
+++ b/src/strings/ar.json
@@ -729,7 +729,7 @@
"XmlTvSportsCategoriesHelp": "البرامج من هذه التصنيفات ستعرض كبرامج رياضية. إفصل الإدخالات المتعددة برمز \"|\".",
"Yesterday": "البارحة",
"ConfirmDeleteImage": "حذف الصورة؟",
- "ConfigureDateAdded": "قم بإعداد كيفية تحديد البيانات الوصفية ل \"تاريخ الإضافة\" في لوحة المعلومات> المكتبات> إعدادات NFO",
+ "ConfigureDateAdded": "قم بإعداد كيفية تحديد البيانات الوصفية ل \"تاريخ الإضافة\" في لوحة المعلومات> المكتبات> العرض",
"Composer": "ألحان",
"CommunityRating": "تقييم الجمهور",
"ColorTransfer": "نقل اللون",
@@ -886,7 +886,7 @@
"ButtonTogglePlaylist": "قائمة التشغيل",
"BoxSet": "طقم",
"ButtonSplit": "تقسيم",
- "AllowFfmpegThrottlingHelp": "عند تفعيلها؛ فسوف تتوقف عملية الترميز transcoding توقفا مؤقتا كلما تقدمت العملية عن موضع التشغيل بنسبة كافية، تهدف هذه الخاصية إلى التقليل من استهلاك الطاقة، وتكون ذات منفعة كبيرة عندما تتم عملية المشاهدة بانتظام دون القفز عدة دقائق في المشاهدة ما بين الحينة والأخرى. كما ينطبق الأمر ذاته على عملية نسخ الملف إلى حاوية أخرى لتتوافق مع الجهاز remuxing.",
+ "AllowFfmpegThrottlingHelp": "عند تقدم اي تحويل كود او remux لمسافة مناسبة امام نقطة اعادة التشغيل الحالية، اوقف العملية حتى يتم استهلاك موارد أقل. هذا سوف يكون مفيد عندما يتم المشاهدة بدون التقدم بشكل مستمر. قم بإطفاء الخاصية هذه عندما تواجه مشاكل في إعادة التشغيل.",
"InstallingPackage": "تثبيت {0} (الإصدار {1})",
"Images": "الصور",
"Identify": "التعرف على الوسائط",
@@ -1052,7 +1052,7 @@
"DashboardArchitecture": "المعمارية: {0}",
"DailyAt": "يومياً على {0}",
"ClearQueue": "مسح القائمة المؤقتة",
- "Bwdif": "BWDIF",
+ "Bwdif": "فلتر بوب ويفر لإزالة التداخل (BWDIF)",
"ButtonPlayer": "المشغل",
"ButtonCast": "إرسال وسائط إلى جهاز",
"HeaderSyncPlayTimeSyncSettings": "تزامن الوقت",
@@ -1722,7 +1722,7 @@
"LabelEnableAudioVbrHelp": "معدل البِت المتغير ينتج على جودة أفضل مقارنة بمعدل البت المتوسط، ولكن في بعض الحالات النادرة قد يسبب مشاكل في التخزين المؤقت والتوافق.",
"LabelSegmentKeepSecondsHelp": "الزمن بالثواني الذي يجب الاحتفاظ به للشرائح بعد أن يتم تحميلها من قبل العميل. يعمل هذا ألأعداد فقط إذا كان حذف الشرائح مفعلًا.",
"AiTranslated": "مترجمة من قبل ذكاء اسطناعي",
- "SelectAudioNormalizationHelp": "كسب الالبوم-تعديل الصوت لكل مسار لكي يعملون بنفس مستوى- كسب الالبوم- تعديل مستوى الصوت لكل المسارات في البوم واحد مع ابقاء على النطاق الديناميكي للألبوم.",
+ "SelectAudioNormalizationHelp": "كسب الالبوم-تعديل الصوت لكل مسار لكي يعملون بنفس المستوى- كسب الالبوم- تعديل مستوى الصوت لكل المسارات في البوم واحد مع ابقاء على النطاق الديناميكي للألبوم. التحويل بين (إيقاف) والخيارات الاخرى يتطلب إعادة تشغيل playback الحالي.",
"ButtonEditUser": "تعديل مستخدم",
"AllowSubtitleManagement": "اسمح لهذا المستخدم تعديل الترجمات",
"HeaderDeleteSeries": "حذف مسلسل",
@@ -1826,5 +1826,28 @@
"LabelDisableVbrAudioEncoding": "تعطيل VBR لترميز الصوت",
"HeaderNextVideo": "الفيديو التالي",
"LabelDevice": "الجهاز",
- "LabelEnablePlugin": "تفعيل البرنامج الإضافي"
+ "LabelEnablePlugin": "تفعيل البرنامج الإضافي",
+ "CopyLogSuccess": "محتويات السجل نُسخت بنجاح.",
+ "Illustrator": "الرسام",
+ "Creator": "المنشئ",
+ "HeaderUploadLyrics": "رفع كلمات المحتوى",
+ "DeleteServerConfirmation": "هل أنت متاكد انك تريد حذف الخادم؟",
+ "HeaderVideoAdvanced": "فيديو متقدم",
+ "HeaderNewPlaylist": "قائمة تشغيل جديدة",
+ "HeaderNoLyrics": "لم يتم ايجاد كلمات محتوى",
+ "LabelSelectPreferredTranscodeVideoAudioCodec": "برنامج ترميز الصوت المفضل في تشغيل الفيديو",
+ "LabelBackdropScreensaverInterval": "فاصل شاشة التوقف الخلفية",
+ "LabelAllowFmp4TranscodingContainer": "السماح بحاوية تحويل الترميز بـfMP4",
+ "LabelAudioTagSettings": "إعدادات العلامات الصوتية",
+ "LabelCustomTagDelimiters": "فاصل العلامة المخصصة",
+ "LabelAlbumGain": "كسب الألبوم",
+ "Inker": "الحبر",
+ "LabelAllowContentWithTags": "السماح بالعناصر مع العلامات",
+ "DisplayLoadError": "خطأ حصل اثناء تحميل بيانات إعدادات العرض.",
+ "HeaderPreviewLyrics": "استعراض كلمات المحتوى",
+ "LabelAlwaysRemuxMp3AudioFiles": "السماح دائماً بعمل remux لملفات MP3 الصوتية",
+ "LabelAlwaysRemuxFlacAudioFiles": "السماح دائماً بعمل remux لملفات FLAC الصوتية",
+ "LabelAllowStreamSharing": "السماح بمشاركة البث",
+ "HeaderLyricDownloads": "تحميلات كلمات المحتوى",
+ "HeaderMediaSegmentActions": "إجراءات قطاع الوسائط"
}
diff --git a/src/strings/be-by.json b/src/strings/be-by.json
index 8b263a77ca..be3d706cc9 100644
--- a/src/strings/be-by.json
+++ b/src/strings/be-by.json
@@ -1955,5 +1955,9 @@
"LabelMediaSegmentProviders": "Пастаўшчыкі сегментаў медыяфайлаў",
"MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.",
"MediaSegmentType.Commercial": "Рэклама",
- "MediaSegmentAction.None": "Няма"
+ "MediaSegmentAction.None": "Няма",
+ "HeaderNextVideo": "Наступнае відэа",
+ "HeaderPageNotFound": "Старонка не знойдзена",
+ "LabelSaveTrickplayLocally": "Захаваць выявы trickplay побач з медыяфайламі",
+ "LabelSaveTrickplayLocallyHelp": "Захаванне выяў trickplay у тэчках з медыяфайламі дазволіць размясціць іх побач з вашымі медыяфайламі для зручнай міграцыі і доступу."
}
diff --git a/src/strings/cs.json b/src/strings/cs.json
index d5f8a9d74d..26931bc3f6 100644
--- a/src/strings/cs.json
+++ b/src/strings/cs.json
@@ -2012,5 +2012,7 @@
"MetadataNfoLoadError": "Načtení nastavení metadat v souborech NFO se nezdařilo",
"HeaderPageNotFound": "Stránka nebyla nalezena",
"PageNotFound": "Toto není stránka, kterou hledáš.",
- "SettingsPageLoadError": "Načtení stránky nastavení se nezdařilo"
+ "SettingsPageLoadError": "Načtení stránky nastavení se nezdařilo",
+ "RetryWithGlobalSearch": "Zkusit hledat globálně",
+ "StreamCountExceedsLimit": "Počet streamů překračuje limit"
}
diff --git a/src/strings/da.json b/src/strings/da.json
index 8c74cccd5d..514f3a1be4 100644
--- a/src/strings/da.json
+++ b/src/strings/da.json
@@ -1993,7 +1993,7 @@
"LabelMediaSegmentProviders": "Mediesegment tilbydere",
"MediaSegmentProvidersHelp": "Aktiver og arranger dine foretrukne mediesegment-tilbydere efter prioritering.",
"DeleteServerConfirmation": "Er du sikker på at du ønsker slette denne server?",
- "AutoSubtitleStylingHelp": "Denne tilstand vil automatisk skifte mellem oprindelig og brugerdefineret stylings-mekanismer baseret på din enheds type.",
+ "AutoSubtitleStylingHelp": "Denne tilstand vil automatisk skifte mellem oprindelig og brugerdefineret undertekst stylings-mekanismer baseret på din enheds type.",
"Custom": "Brugerdefineret",
"CustomSubtitleStylingHelp": "Undertekse styling vil virke på de fleste enheder, men kommer med en præstations pris.",
"LabelSubtitleStyling": "Undertekst Styling",
diff --git a/src/strings/el.json b/src/strings/el.json
index 9b72bfbced..df50ffa1b5 100644
--- a/src/strings/el.json
+++ b/src/strings/el.json
@@ -1885,7 +1885,7 @@
"Lyric": "Στίχος",
"LogoScreensaver": "Λογότυπο Προφύλαξης Οθόνης",
"MediaSegmentProvidersHelp": "Ενεργοποίηση και ταξινόμηση των προτιμωμένων παροχέων τμημάτων πολυμέσων σε σειρά προτεραιότητας.",
- "LibraryInvalidItemIdError": "H βιβλιοθήκη βρίσκεται σε μη έγκυρη κατάσταση και δεν μπορεί να τροποποιηθεί. Πιθανότατα αντιμετοπίζετε σφάλμα: η διαδρομή στην βάση δεδομένη δεν είναι η σωστή στον δίσκο.",
+ "LibraryInvalidItemIdError": "H βιβλιοθήκη βρίσκεται σε μη έγκυρη κατάσταση και δεν μπορεί να τροποποιηθεί. Πιθανότατα αντιμετωπίζετε σφάλμα: η διαδρομή στην βάση δεδομένη δεν είναι η σωστή στο σύστημα αρχείων.",
"Lyrics": "Στίχοι",
"MediaInfoRotation": "Προσανατολισμός",
"LimitSupportedVideoResolution": "Περιορισμός μέγιστης υποστηριζόμενης ανάλυσης βίντεο",
@@ -1963,5 +1963,6 @@
"PlaybackError.MEDIA_NOT_SUPPORTED": "Η αναπαραγωγή απέτυχε διότι το μέσο δεν υποστηρίζεται από αυτό το πρόγραμμα.",
"PlaybackError.MEDIA_DECODE_ERROR": "Η αναπαραγωγή απέτυχε λόγω σφάλματος στην αποκωδικοποίηση του μέσου.",
"MetadataImagesLoadError": "Αποτυχία φόρτωσης ρυθμίσεων των μεταδεδομένων",
- "PlaybackError.FATAL_HLS_ERROR": "Παρουσιάστηκε κρίσιμο σφάλμα στην ροή HLS."
+ "PlaybackError.FATAL_HLS_ERROR": "Παρουσιάστηκε κρίσιμο σφάλμα στην ροή HLS.",
+ "SettingsPageLoadError": "Αποτυχία φόρτωσης σελίδας ρυθμίσεων"
}
diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json
index 5452e2effd..9fd676af1c 100644
--- a/src/strings/en-gb.json
+++ b/src/strings/en-gb.json
@@ -1719,7 +1719,7 @@
"Short": "Short",
"HeaderPerformance": "Performance",
"LabelParallelImageEncodingLimit": "Parallel image encoding limit",
- "LabelParallelImageEncodingLimitHelp": "Maximum number of image encodings that are allowed to run in parallel. Setting this to 0 will choose a limit based on your systems core count.",
+ "LabelParallelImageEncodingLimitHelp": "Maximum number of image encodings that are allowed to run in parallel. Leaving this empty will choose a limit based on your systems core count.",
"LabelEnableAudioVbr": "Enable VBR audio encoding",
"LabelEnableAudioVbrHelp": "Variable bitrate offers better quality to average bitrate ratio, but in some rare cases may cause buffering and compatibility issues.",
"LabelTonemappingMode": "Tone mapping mode",
@@ -1880,7 +1880,7 @@
"LabelSelectPreferredTranscodeVideoAudioCodec": "Preferred transcode audio codec in video playback",
"Letterer": "Letterer",
"LibraryScanFanoutConcurrency": "Parallel library scan tasks limit",
- "LibraryScanFanoutConcurrencyHelp": "Maximum number of parallel tasks during library scans. Setting this to 0 will choose a limit based on your systems core count. WARNING: Setting this number too high may cause issues with network file systems; if you encounter problems lower this number.",
+ "LibraryScanFanoutConcurrencyHelp": "Maximum number of parallel tasks during library scans. Leaving this empty will choose a limit based on your system's core count. WARNING: Setting this number too high may cause issues with network file systems; if you encounter problems lower this number.",
"Penciller": "Penciler",
"PlaylistError.AddFailed": "Error adding to playlist",
"PlaylistError.CreateFailed": "Error creating playlist",
@@ -2011,5 +2011,7 @@
"DisplayLoadError": "An error occurred while loading display configuration data.",
"HeaderPageNotFound": "Page not found",
"PageNotFound": "This is not the page you are looking for.",
- "MetadataImagesLoadError": "Failed to load metadata settings"
+ "MetadataImagesLoadError": "Failed to load metadata settings",
+ "SettingsPageLoadError": "Failed to load settings page",
+ "RetryWithGlobalSearch": "Retry with a global search"
}
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 0204b9817d..2d6678c325 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -1451,6 +1451,7 @@
"ReplaceExistingImages": "Replace existing images",
"ReplaceTrickplayImages": "Replace existing trickplay images",
"Retry": "Retry",
+ "RetryWithGlobalSearch": "Retry with a global search",
"Reset": "Reset",
"ResetPassword": "Reset Password",
"ResolutionMatchSource": "Match Source",
@@ -1530,6 +1531,7 @@
"StoryArc": "Story Arc",
"StopPlayback": "Stop playback",
"StopRecording": "Stop recording",
+ "StreamCountExceedsLimit": "The number of streams exceeds the limit",
"Studio": "Studio",
"Studios": "Studios",
"Subtitle": "Subtitle",
diff --git a/src/strings/fi.json b/src/strings/fi.json
index 407686b373..6667c81d7b 100644
--- a/src/strings/fi.json
+++ b/src/strings/fi.json
@@ -1664,8 +1664,8 @@
"MediaInfoVideoRangeType": "Videon aluetyyppi",
"LabelVideoRangeType": "Videon aluetyyppi",
"VideoRangeTypeNotSupported": "Videon aluetyyppiä ei tueta",
- "LabelVppTonemappingContrastHelp": "Käytä kontrastin vahvistusta VPP-sävykartoituksen kanssa. Suositus- ja oletusarvo on 1.",
- "LabelVppTonemappingBrightnessHelp": "Käytä kirkkauden vahvistusta VPP-sävykartoituksen kanssa. Suositus- ja oletusarvot ovat 10 ja 0.",
+ "LabelVppTonemappingContrastHelp": "Käytä kontrastin vahvistusta VPP-sävykartoituksen kanssa. Suositusarvo on 1.",
+ "LabelVppTonemappingBrightnessHelp": "Käytä kirkkauden vahvistusta VPP-sävykartoituksen kanssa. Suositusarvo on 16.",
"LabelVppTonemappingContrast": "VPP-sävykartoituksen kontrastin vahvistus",
"LabelVppTonemappingBrightness": "VPP-sävykartoituksen kirkkauden vahvistus",
"IgnoreDtsHelp": "Valinnan poistaminen voi korjata joitakin ongelmia, kuten puuttuvan äänen kanavilla joilla on erilliset ääni- ja videovirrat.",
@@ -1754,7 +1754,7 @@
"LogLevel.Critical": "Kriittinen",
"LogLevel.None": "Ei mitään",
"HeaderEpisodesStatus": "Jaksojen tila",
- "AllowSegmentDeletionHelp": "Poista vanhat osiot kun ne on lähetetty päätteelle. Tämän ansiosta transkoodattua tiedostoa ei tarvitse säilyttää kokonaan. Toimii vain rajoituksen ollessa käytössä. Poista käytöstä, jos kohtaat toisto-ongelmia.",
+ "AllowSegmentDeletionHelp": "Poista vanhat osiot kun ne on ladattu päätteelle. Tämän ansiosta transkoodattua tiedostoa ei tarvitse säilyttää kokonaan. Poista käytöstä, jos kohtaat toisto-ongelmia.",
"AllowSegmentDeletion": "Poista osiot",
"LabelThrottleDelaySeconds": "Rajoita kun on kulunut",
"LabelThrottleDelaySecondsHelp": "Aika sekunneissa, jonka jälkeen transkoodausta rajoitetaan. Tämän on oltava riittävän suuri, jotta päätelaite kykenee ylläpitämään reilua puskuria. Toimii vain rajoituksen ollessa käytössä.",
@@ -1920,11 +1920,11 @@
"AllowTonemappingSoftwareHelp": "Sävykartoitus voi muuttaa videon dynaamista aluetta HDR:stä SDR:ään säilyttäen silti kuvan yksityiskohdat ja värin, jotka ovat erittäin tärkeitä alkuperäisen kohtauksen tiedon säilyttämiseksi. Tällä hetkellä se toimii ainoastaan 10bit HDR10, -HLG, ja DoVi-videoiden kanssa.",
"Editor": "Ohjaus",
"Letterer": "Kirjoittaja",
- "LibraryScanFanoutConcurrencyHelp": "Samanaikaisten suoritettavien kirjastoskannausten maksimimäärä. Mikäli tämä arvo on asetettu 0, määrä valitaan järjestelmän prosessorin säikeiden lukumäärän mukaan. VAROITUS: Tämän arvon asettaminen liian korkeaksi voi aiheuttaa ongelmia verkkotiedostojärjestelmissä. Jos koet ongelmatilanteista, laske tätä numeroa.",
+ "LibraryScanFanoutConcurrencyHelp": "Samanaikaisten suoritettavien kirjastoskannausten maksimimäärä. Mikäli tämä arvo on jätetty tyhjäksi, määrä valitaan järjestelmän prosessorin säikeiden lukumäärän mukaan. VAROITUS: Tämän arvon asettaminen liian korkeaksi voi aiheuttaa ongelmia verkkotiedostojärjestelmissä. Jos koet ongelmatilanteista, laske tätä numeroa.",
"SaveLyricsIntoMediaFoldersHelp": "Sanoitusten tallentaminen äänitiedoston kanssa samaan sijaintiin helpottaa niiden hallintaa.",
"SelectPreferredTranscodeVideoAudioCodecHelp": "Valitse ensisijainen äänikoodekki videomateriaalin transkoodaamiseen. Jos ensisijainen koodekki ei ole tuettu, serveri käyttää seuraavaksi parasta koodekkia.",
"LabelTrickplayAccelEncoding": "Käyttöönota rautakiihdytetty MJPEG enkoodaus",
- "LabelTrickplayAccelEncodingHelp": "Tällä hetkellä ainoastaan käytettävissä QSV, VAAPI ja VideoToolbox, tällä valinnalla ei ole vaikutusta muihin rautakiihdytysmetodeihin.",
+ "LabelTrickplayAccelEncodingHelp": "Tällä hetkellä ainoastaan käytettävissä QSV, VA-API, VideoToolbox ja RKMPP. Tällä valinnalla ei ole vaikutusta muihin rautakiihdytysmetodeihin.",
"HeaderVideoAdvanced": "Edistynyt video",
"PlaylistPublicDescription": "Salli tämän soittolistan katsominen jokaiselle kirjautuneelle käyttäjälle.",
"DateModified": "Muokkauspäivämäärä",
@@ -1985,5 +1985,12 @@
"Retry": "Yritä uudelleen",
"Reset": "Nollaa",
"ReplaceTrickplayImages": "Korvaa nykyiset trickplay kuvat",
- "RenderPgsSubtitleHelp": "Renderöidäänkö PGS tekstitykset laitteen toimesta. Tällä voidaan välttää raskas tekstitysten poltto kiinteästi kuvaan palvelimen toimesta, mutta lisätään laitteen renderöintikuormaa."
+ "RenderPgsSubtitleHelp": "Renderöidäänkö PGS tekstitykset laitteen toimesta. Tällä voidaan välttää raskas tekstitysten poltto kiinteästi kuvaan palvelimen toimesta, mutta lisätään laitteen renderöintikuormaa.",
+ "HeaderPageNotFound": "Sivua ei löytynyt",
+ "PageNotFound": "Tämä ei ole etsimäsi sivu.",
+ "CopyLogSuccess": "Lokitietojen kopiointi onnistui.",
+ "DeleteServerConfirmation": "Haluatko varmasti poistaa tämän palvelimen?",
+ "LabelDevice": "Laite",
+ "MetadataImagesLoadError": "Metadata-asetusten lataus epäonnistui",
+ "LibraryNameInvalid": "Kirjastolla tulee olla nimi."
}
diff --git a/src/strings/fr-ca.json b/src/strings/fr-ca.json
index 484f41f6cf..47e22d0824 100644
--- a/src/strings/fr-ca.json
+++ b/src/strings/fr-ca.json
@@ -215,7 +215,7 @@
"LabelVideo": "Vidéo",
"DashboardArchitecture": "Architecture : {0}",
"DashboardOperatingSystem": "Système d'exploitation : {0}",
- "ConfigureDateAdded": "Définissez la façon dont la métadonnée \"Date d'ajout\" est déterminée dans le Tableau de bord > Bibliothèques > Paramètres NFO",
+ "ConfigureDateAdded": "Définissez la façon dont la métadonnée \"Date d'ajout\" est déterminée dans le Tableau de bord > Bibliothèques > Affichage",
"Composer": "Compositeur(trice)",
"CommunityRating": "Évaluation de la communauté",
"ColorTransfer": "Transfert de couleur",
@@ -1183,7 +1183,7 @@
"LabelNumberOfGuideDays": "Nombre de jours de guide à télécharger",
"LabelOpenclDeviceHelp": "Le périphérique OpenCL qui sera utilisé pour le « tone mapping » HDR. Le chiffre a gauche du point est le numéro de plateforme, et celui de droite est le numéro de périphérique sur la plateforme. La valeur par défaut est 0.0. Le fichier d’application FFmpeg prenant en charge l’accélération OpenCL est requis.",
"LabelParentNumber": "Numéro parent",
- "LabelParallelImageEncodingLimitHelp": "Nombre maximal d’encodages d’images qui peuvent être exécutés en parallèle. Une valeur de 0 entraînera une sélection automatique d’une limite selon le nombre de coeurs de votre système.",
+ "LabelParallelImageEncodingLimitHelp": "Nombre maximal d’encodages d’images qui peuvent être exécutés en parallèle. Une valeur vide entraînera une sélection automatique d’une limite selon le nombre de coeurs de votre système.",
"LabelPasswordResetProvider": "Fournisseur de récupération de mot de passe",
"LabelPlayDefaultAudioTrack": "Lire la piste audio par défaut peu importe la langue",
"LabelPostProcessor": "Application de post-traitement",
@@ -1353,7 +1353,7 @@
"Letterer": "Lettreur",
"Next": "Suivant",
"LibraryScanFanoutConcurrency": "Limite de tâches de scan de médiathèque en parallèle",
- "LibraryScanFanoutConcurrencyHelp": "Nombre maximal de tâches en parallèle pour les scans de médiathèque. Une valeur de 0 laissera le système choisir une limite en fonction du nombre de coeurs. ATTENTION: Définir une valeur trop élevée peut causer des problèmes avec les systèmes de fichers réseau. Si vous avez des problèmes, réduisez cette valeur.",
+ "LibraryScanFanoutConcurrencyHelp": "Nombre maximal de tâches en parallèle pour les scans de médiathèque. Une valeur vide laissera le système choisir une limite en fonction du nombre de coeurs. ATTENTION: Définir une valeur trop élevée peut causer des problèmes avec les systèmes de fichers réseau. Si vous avez des problèmes, réduisez cette valeur.",
"OptionEveryday": "Tous les jours",
"OptionForceRemoteSourceTranscoding": "Forcer le transcodage pour les sources de média externes comme la télé en direct",
"OptionHasThemeVideo": "Générique",
@@ -1943,5 +1943,20 @@
"LabelProcessPriorityHelp": "Un réglage inférieur ou supérieur déterminera la manière dont le processeur donne la priorité au processus de génération de trickplay ffmpeg par rapport aux autres processus. Si vous remarquez un ralentissement lors de la génération d'images trickplay mais que vous ne souhaitez pas arrêter complètement leur génération, essayez de réduire ce ralentissement en modifiant le nombre de threads.",
"LabelTileWidthHelp": "Nombre maximum d'images par tuile dans la direction X.",
"LabelTileHeight": "Hauteur des tuiles",
- "LabelTileHeightHelp": "Nombre maximum d'images par tuile dans la direction Y."
+ "LabelTileHeightHelp": "Nombre maximum d'images par tuile dans la direction Y.",
+ "SettingsPageLoadError": "Échec du chargement de la page des paramètres",
+ "RetryWithGlobalSearch": "Réessayez avec une recherche globale",
+ "HeaderPageNotFound": "Page introuvable",
+ "PageNotFound": "Ceci n'est pas la page que vous cherchez.",
+ "MetadataNfoLoadError": "Échec du chargement des paramètres NFO des métadonnées",
+ "CopyLogSuccess": "Le contenu des journaux a été copié avec succès.",
+ "Retry": "Réessayer",
+ "DisplayLoadError": "Une erreur est survenue lors du chargement des données de configuration d'affichage.",
+ "LogLoadFailure": "Échec du chargement du fichier journal. Il est possible qu'il soit encore en cours d'écriture.",
+ "DeleteServerConfirmation": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
+ "LastActive": "Dernière activation",
+ "LabelDevice": "Appareil",
+ "MetadataImagesLoadError": "Échec du chargement des paramètres de métadonnées",
+ "LibraryNameInvalid": "Le nom de la bibliothèque ne peut pas être vide.",
+ "StreamCountExceedsLimit": "Le nombre de flux dépasse la limite"
}
diff --git a/src/strings/fr.json b/src/strings/fr.json
index d5d63e96e8..fc324e3d78 100644
--- a/src/strings/fr.json
+++ b/src/strings/fr.json
@@ -2012,5 +2012,7 @@
"LibraryNameInvalid": "Le nom de la bibliothèque ne peut pas être vide.",
"HeaderPageNotFound": "Page introuvable",
"PageNotFound": "Ceci n'est pas la page que vous cherchez.",
- "SettingsPageLoadError": "Échec du chargement de la page des paramètres"
+ "SettingsPageLoadError": "Échec du chargement de la page des paramètres",
+ "RetryWithGlobalSearch": "Réessayez avec une recherche globale",
+ "StreamCountExceedsLimit": "Le nombre de flux dépasse la limite"
}
diff --git a/src/strings/hr.json b/src/strings/hr.json
index fb84ffc847..2b82e04a8f 100644
--- a/src/strings/hr.json
+++ b/src/strings/hr.json
@@ -1133,7 +1133,7 @@
"ClearQueue": "Očisti red",
"Bwdif": "BWDIF",
"ButtonPlayer": "Reproduktor",
- "AllowTonemappingHelp": "Tonsko preslikavanje može transformirati dinamički raspon videozapisa iz HDR u SDR zadržavajući detalje slike i boje, što su vrlo važne informacije za predstavljanje izvorne scene. Trenutačno radi samo pri transkodiranju videozapisa s ugrađenim HDR10 ili HLG metapodacima. Ako reprodukcija nije glatka ili ne uspije, razmislite o isključivanju odgovarajućeg hardverskog dekodera HDR10 ili HLG videozapisa. Ovo zahtijeva odgovarajuće OpenCL ili CUDA runtime.",
+ "AllowTonemappingHelp": "Tonsko preslikavanje može transformirati dinamički raspon videozapisa iz HDR u SDR zadržavajući detalje slike i boje, što su vrlo važne informacije za predstavljanje izvorne scene. Trenutačno radi samo pri prekodiranju videozapisa s ugrađenim HDR10 ili HLG metapodatcima. Ako reprodukcija nije glatka ili ne uspije, razmislite o isključivanju odgovarajućeg hardverskog dekodera HDR10 ili HLG videozapisa. Ovo zahtijeva odgovarajući GPGPU runtime.",
"LabelCreateHttpPortMap": "Omogući automatsko mapiranje ulaza za HTTP i HTTPS promet.",
"LabelChromecastVersion": "Google Cast verzija",
"LabelCertificatePasswordHelp": "Ako Vaš certifikat zahtjeva lozinku, molimo unesite je ovdje.",
@@ -1570,7 +1570,7 @@
"DisplayLoadError": "Dogodila se pogreška tijekom prikazivanja podataka za konfiguraciju.",
"EnableLibrary": "Uključite biblioteku",
"EnableLibraryHelp": "Isključivanje bibliotekeće ju sakriti od svih korisnika.",
- "AlwaysBurnInSubtitleWhenTranscoding": "Uvijek ureži titlove tijekom transkodiranja",
+ "AlwaysBurnInSubtitleWhenTranscoding": "Uvijek ureži titlove tijekom prekodiranja",
"AlwaysRemuxFlacAudioFilesHelp": "Ako imate datoteke koje Vaš preglednik ne želi izvoditi ili kada neprecizno izračuna vremenske oznake, uključite ovo kao zaobilazak.",
"AlwaysRemuxMp3AudioFilesHelp": "Ako imate datoteke za koje Vaš preglednik neprecizno izračunava vremenske oznake, uključite ovo kao zaobilazak.",
"EditLyrics": "Uredi tekst pjesme",
diff --git a/src/strings/hu.json b/src/strings/hu.json
index bf62e8b964..b5776e06b7 100644
--- a/src/strings/hu.json
+++ b/src/strings/hu.json
@@ -1976,5 +1976,21 @@
"LabelTrickplayKeyFrameOnlyExtractionHelp": "Csak kulcsképkockák kinyerése a jelentősen gyorsabb számítás érdekében, de kevésbé pontos időzítéssel. Ha a beállított hardveres dekódoló nem támogatja ezt a módot, akkor a szoftveres dekódoló lesz használva.",
"DeleteServerConfirmation": "Biztos, hogy törli ezt a kiszolgálót?",
"VideoCodecTagNotSupported": "A videókodek-címke nem támogatott",
- "LabelTrickplayKeyFrameOnlyExtraction": "Képek előállítása csak kulcsképkockákból"
+ "LabelTrickplayKeyFrameOnlyExtraction": "Képek előállítása csak kulcsképkockákból",
+ "HeaderPageNotFound": "Az oldal nem található",
+ "LabelMediaSegmentProviders": "Médiaszegmens szolgáltatók",
+ "MediaSegmentProvidersHelp": "Engedélyezd és tedd sorba a médiaszegmens szolgáltatókat preferencia (prioritás) alapján.",
+ "MetadataNfoLoadError": "Nem sikerült a metaadat NFO beállításokat betölteni",
+ "PageNotFound": "Ez nem az oldal, amit keresel.",
+ "SettingsPageLoadError": "Nem sikerült betölteni a beállítások oldalt",
+ "CustomSubtitleStylingHelp": "A feliratstílus működni fog a legtöbb eszközön, de további teljesítményt igényel.",
+ "LabelSubtitleStyling": "Feliratstílus",
+ "CopyLogSuccess": "A napló tartalma sikeresen másolva lett.",
+ "DisplayLoadError": "Egy hibába ütköztünk a kijelző beállításainak betöltése közben.",
+ "LabelDevice": "Eszköz",
+ "LastActive": "Legutóbb aktív",
+ "PreferNonstandardArtistsTagHelp": "Használja a nem szabványos ARTISTS címkét az ARTIST címke helyett, ha elérhető.",
+ "Penciller": "Grafikus",
+ "MetadataImagesLoadError": "Nem sikerült betölteni a metaadat beállításokat",
+ "LibraryNameInvalid": "A könyvtár neve nem lehet üres."
}
diff --git a/src/strings/it.json b/src/strings/it.json
index 778b1b33c3..c151801b0c 100644
--- a/src/strings/it.json
+++ b/src/strings/it.json
@@ -2012,5 +2012,6 @@
"MetadataNfoLoadError": "Errore nel caricamento dei metadati NFO",
"HeaderPageNotFound": "Pagina non trovata",
"PageNotFound": "Questa non è la pagina che stai cercando.",
- "SettingsPageLoadError": "Errore nel caricamento della pagina di configurazione"
+ "SettingsPageLoadError": "Errore nel caricamento della pagina di configurazione",
+ "RetryWithGlobalSearch": "Prova di nuovo con la ricerca globale"
}
diff --git a/src/strings/lv.json b/src/strings/lv.json
index 9d5d802149..c63d2db07f 100644
--- a/src/strings/lv.json
+++ b/src/strings/lv.json
@@ -689,7 +689,7 @@
"LabelReleaseDate": "Izlaiduma datums",
"LabelPreferredSubtitleLanguage": "Ieteicamā subtitru valoda",
"LabelPlayerDimensions": "Atskaņotāja dimensijas",
- "LabelParentalRating": "Vecāku reitings",
+ "LabelParentalRating": "Vecuma reitings",
"LabelMonitorUsers": "Uzraudzīt aktivitāti no",
"LabelMinResumePercentageHelp": "Vienumi tiek uzskatīti par neatskaņotiem, ja apturēti pirms šī laika.",
"LabelMinResumePercentage": "Minimālais turpināšanas procents",
@@ -1349,7 +1349,7 @@
"LabelDummyChapterCountHelp": "Maksimālais nodaļu attēlu skaits, kas tiks ekstraktēts no katra multivides faila.",
"LabelChapterImageResolutionHelp": "Izvilkto nodaļu attēlu izšķirtspēja. Šīs vērtības maiņa neietekmēs esošās fiktīvās nodaļas.",
"LabelParallelImageEncodingLimit": "Paralēlas attēlu kodēšanas limits",
- "LabelParallelImageEncodingLimitHelp": "Maksimālais attēlu kodējumu skaits, kurus atļauts palaist paralēli. Nosakot 0, tiks izvēlēts ierobežojums, kas balstīts uz jūsu sistēmas kodolu skaitu.",
+ "LabelParallelImageEncodingLimitHelp": "Maksimālais attēlu kodējumu skaits, kurus atļauts palaist paralēli. Atstājot tukšu, tiks izvēlēts ierobežojums, kas balstīts uz jūsu sistēmas kodolu skaitu.",
"HeaderDummyChapter": "Nodaļu attēli",
"EnableCardLayout": "Padarīt redzamu CardBox",
"MessageConfirmDeleteGuideProvider": "Vai tiešām vēlaties izdzēst šo ceļveža pakalpojumu sniedzēju?",
@@ -1748,7 +1748,7 @@
"LabelBuildVersion": "Kompilācijas versija",
"SelectAudioNormalizationHelp": "Audioceliņa pastiprinājums — pielāgo katra celiņa skaļumu, lai tie tiktu atskaņoti ar tādu pašu skaļumu. Albuma pastiprinājums - pielāgo visus albuma audio, saglabājot albuma dinamisko diapazonu. Pārslēdzoties starp \"Izslēgts\" un pārējām iespējām, ir nepieciešams pārstartēt pašreizējo atskaņošanu.",
"LibraryScanFanoutConcurrency": "Paralēlās bibliotēkas skenēšanas uzdevumu ierobežojums",
- "LibraryScanFanoutConcurrencyHelp": "Maksimālais paralēlo uzdevumu skaits bibliotēkas skenēšanas laikā. Iestatot 0, tiks izvēlēts ierobežojums, pamatojoties uz jūsu sistēmas kodolu skaitu. BRĪDINĀJUMS: Pārāk liels skaitlis var radīt problēmas tīkla failu sistēmām. Ja novērojat problēmas, samaziniet šo skaitli.",
+ "LibraryScanFanoutConcurrencyHelp": "Maksimālais paralēlo uzdevumu skaits bibliotēkas skenēšanas laikā. Atstājot tukšu, tiks izvēlēts ierobežojums, pamatojoties uz jūsu sistēmas kodolu skaitu. BRĪDINĀJUMS: Pārāk liels skaitlis var radīt problēmas tīkla failu sistēmām. Ja novērojat problēmas, samaziniet šo skaitli.",
"PlaylistPublic": "Atļaut publisku piekļuvi",
"PlaylistPublicDescription": "Ļaut šo atskaņošanas sarakstu skatīt jebkuram autentificētam lietotājam.",
"Rate": "Vertējums",
@@ -1946,5 +1946,6 @@
"ExtractTrickplayImagesHelp": "Trickplay attēli līdzinās sadaļu attēliem, bet tie tiek saģenerēti visam satura garumam un tiek lietoti kā priekšskatījums kad ātri ritina cauri video.",
"LabelExtractTrickplayDuringLibraryScan": "Izgūt trickplay attēlus bibliotēkas skenēšanas laikā",
"LabelJpegQualityHelp": "Trickplay attēlu JPEG kompresijas kvalitātes lielums.",
- "LogLoadFailure": "Neizdevās ielādēt žurnālfailu. Iespējams tas tiek aizvien izmantots žurnāla ierakstu saglabāšanai."
+ "LogLoadFailure": "Neizdevās ielādēt žurnālfailu. Iespējams tas tiek aizvien izmantots žurnāla ierakstu saglabāšanai.",
+ "SettingsPageLoadError": "Neizdevās ielādēt iestatījumu lapu"
}
diff --git a/src/strings/nb.json b/src/strings/nb.json
index 32530a24f0..6b64461c89 100644
--- a/src/strings/nb.json
+++ b/src/strings/nb.json
@@ -173,7 +173,7 @@
"HeaderBranding": "Merking",
"HeaderCancelRecording": "Avbryt opptak",
"HeaderCancelSeries": "Avbryt serie",
- "HeaderCastAndCrew": "Skuespillere & mannskap",
+ "HeaderCastAndCrew": "Medvirkende",
"HeaderChannelAccess": "Kanal-tilgang",
"HeaderCodecProfile": "Kodekprofil",
"HeaderCodecProfileHelp": "Kodekprofiler indikerer begrensningene til en enhet ved avspilling av bestemte kodeker. Hvis en begrensning gjelder vil mediet bli omkodet, selv om kodeken er konfigurert for direkteavspilling.",
@@ -1353,7 +1353,7 @@
"MessageGetInstalledPluginsError": "En feil oppstod ved henting av listen over installerte tillegg.",
"MessagePluginInstallError": "En feil oppstod ved installasjon av tillegget.",
"ThumbCard": "Miniatyrbildekort",
- "SpecialFeatures": "Spesialfunksjoner",
+ "SpecialFeatures": "Ekstra innhold",
"PosterCard": "Plakatkort",
"Video": "Video",
"Subtitle": "Undertekst",
@@ -1720,7 +1720,7 @@
"Studio": "Studio",
"SubtitleCyan": "Turkis",
"UserMenu": "Brukermenyen",
- "Featurette": "Novellefilm",
+ "Featurette": "Featurette",
"LabelTonemappingMode": "Tonemappingsmodus",
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Ekstramateriale har ofte det samme innebygde navnet som det opprinnelige materialet. Kryss av for denne for å bruke den innebygde tittelen likevel.",
"LabelSyncPlayNoGroups": "Ingen grupper tilgjengelig",
diff --git a/src/strings/nl.json b/src/strings/nl.json
index 4be83344f5..2ab3480cfc 100644
--- a/src/strings/nl.json
+++ b/src/strings/nl.json
@@ -2011,5 +2011,6 @@
"MetadataNfoLoadError": "Laden van metadata-NFO-instellingen mislukt",
"PageNotFound": "Dit is niet de pagina die je zoekt.",
"HeaderPageNotFound": "Pagina niet gevonden",
- "SettingsPageLoadError": "Laden van instellingenpagina mislukt"
+ "SettingsPageLoadError": "Laden van instellingenpagina mislukt",
+ "RetryWithGlobalSearch": "Alles doorzoeken"
}
diff --git a/src/strings/pl.json b/src/strings/pl.json
index 4d7405ad61..9cabc1fdca 100644
--- a/src/strings/pl.json
+++ b/src/strings/pl.json
@@ -2012,5 +2012,7 @@
"MetadataNfoLoadError": "Nie udało się załadować ustawień metadanych NFO",
"HeaderPageNotFound": "Nie znaleziono strony",
"PageNotFound": "To nie jest strona, której szukasz.",
- "SettingsPageLoadError": "Nie udało się załadować strony ustawień"
+ "SettingsPageLoadError": "Nie udało się załadować strony ustawień",
+ "RetryWithGlobalSearch": "Ponów, korzystając z wyszukiwania globalnego",
+ "StreamCountExceedsLimit": "Liczba strumieni przekracza limit"
}
diff --git a/src/strings/pt-pt.json b/src/strings/pt-pt.json
index 938ec4dd67..56ef3d31fb 100644
--- a/src/strings/pt-pt.json
+++ b/src/strings/pt-pt.json
@@ -2003,5 +2003,7 @@
"Retry": "Tentar novamente",
"LogLoadFailure": "Falha ao carregar o ficheiro de registos. É possível que atualmente esteja a ser escrito.",
"MetadataNfoLoadError": "Falha ao carregar as definições de metadados NFO",
- "SettingsPageLoadError": "Falha ao carregar a página de definições"
+ "SettingsPageLoadError": "Falha ao carregar a página de definições",
+ "RetryWithGlobalSearch": "Tentar novamente com uma pesquisa global",
+ "StreamCountExceedsLimit": "O número de transmissões excede o limite"
}
diff --git a/src/strings/pt.json b/src/strings/pt.json
index 2877c19b1d..232734a07f 100644
--- a/src/strings/pt.json
+++ b/src/strings/pt.json
@@ -1998,5 +1998,7 @@
"DeleteServerConfirmation": "Tens a certeza de que queres eliminar este servidor?",
"LibraryNameInvalid": "O nome da biblioteca não pode estar vazio.",
"MetadataNfoLoadError": "Falha ao carregar as definições de metadados NFO",
- "SettingsPageLoadError": "Falha ao carregar a página de definições"
+ "SettingsPageLoadError": "Falha ao carregar a página de definições",
+ "RetryWithGlobalSearch": "Tentar novamente com uma pesquisa global",
+ "StreamCountExceedsLimit": "O número de transmissões excede o limite"
}
diff --git a/src/strings/ru.json b/src/strings/ru.json
index a30fec3fad..077ae84f60 100644
--- a/src/strings/ru.json
+++ b/src/strings/ru.json
@@ -2012,5 +2012,7 @@
"MetadataImagesLoadError": "Не удалось загрузить настройки метаданных",
"HeaderPageNotFound": "Станица не найдена",
"PageNotFound": "Это не та страница, которую вы искали.",
- "SettingsPageLoadError": "Не удалось загрузить страницу параметров"
+ "SettingsPageLoadError": "Не удалось загрузить страницу параметров",
+ "RetryWithGlobalSearch": "Повторите попытку с помощью глобального поиска",
+ "StreamCountExceedsLimit": "Количество потоков превышает предельное значение"
}
diff --git a/src/strings/sk.json b/src/strings/sk.json
index 8811c156c7..8f9359d60c 100644
--- a/src/strings/sk.json
+++ b/src/strings/sk.json
@@ -2012,5 +2012,7 @@
"MetadataNfoLoadError": "Nepodarilo sa načítať nastavenia NFO metadát",
"HeaderPageNotFound": "Stránka nebola nájdená",
"PageNotFound": "Toto nie je stránka, ktorú hľadáš.",
- "SettingsPageLoadError": "Nepodarilo sa načítať stránku s nastaveniami"
+ "SettingsPageLoadError": "Nepodarilo sa načítať stránku s nastaveniami",
+ "StreamCountExceedsLimit": "Počet streamov prekračuje limit",
+ "RetryWithGlobalSearch": "Skúsiť globálne vyhľadávanie"
}
diff --git a/src/strings/uk.json b/src/strings/uk.json
index fc94c3c0f9..810bf58c3c 100644
--- a/src/strings/uk.json
+++ b/src/strings/uk.json
@@ -2009,5 +2009,7 @@
"MetadataNfoLoadError": "Не вдалося завантажити налаштування метаданих NFO",
"HeaderPageNotFound": "Сторінку не знайдено",
"PageNotFound": "Це не та сторінка, яку ви шукаєте.",
- "SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань"
+ "SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань",
+ "StreamCountExceedsLimit": "Кількість потоків перевищує ліміт",
+ "RetryWithGlobalSearch": "Повторити спробу з глобальним пошуком"
}
diff --git a/src/strings/vi.json b/src/strings/vi.json
index 665833852d..fcfc0dc525 100644
--- a/src/strings/vi.json
+++ b/src/strings/vi.json
@@ -2009,5 +2009,7 @@
"MetadataNfoLoadError": "Tải cài đặt dữ liệu mô tả NFO thất bại",
"PageNotFound": "Đây không phải là trang bạn đang tìm kiếm.",
"HeaderPageNotFound": "Không tìm thấy trang",
- "SettingsPageLoadError": "Tải trang cài đặt thất bại"
+ "SettingsPageLoadError": "Tải trang cài đặt thất bại",
+ "StreamCountExceedsLimit": "Số lượng luồng vượt quá giới hạn",
+ "RetryWithGlobalSearch": "Thử lại với tìm kiếm toàn hệ thống"
}
diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.scss
similarity index 100%
rename from src/themes/appletv/theme.css
rename to src/themes/appletv/theme.scss
diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.scss
similarity index 100%
rename from src/themes/blueradiance/theme.css
rename to src/themes/blueradiance/theme.scss
diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.scss
similarity index 100%
rename from src/themes/dark/theme.css
rename to src/themes/dark/theme.scss
diff --git a/src/themes/light/theme.css b/src/themes/light/theme.scss
similarity index 100%
rename from src/themes/light/theme.css
rename to src/themes/light/theme.scss
diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.scss
similarity index 100%
rename from src/themes/purplehaze/theme.css
rename to src/themes/purplehaze/theme.scss
diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.scss
similarity index 100%
rename from src/themes/wmc/theme.css
rename to src/themes/wmc/theme.scss
diff --git a/src/utils/dashboard.js b/src/utils/dashboard.js
index 46c181910d..9b468730d6 100644
--- a/src/utils/dashboard.js
+++ b/src/utils/dashboard.js
@@ -61,7 +61,7 @@ export async function serverAddress() {
let config;
try {
config = await resp.json();
- } catch (err) {
+ } catch {
return;
}
diff --git a/src/utils/number.ts b/src/utils/number.ts
index 553280c1fe..af7e5f7307 100644
--- a/src/utils/number.ts
+++ b/src/utils/number.ts
@@ -9,6 +9,7 @@ function toLocaleStringSupportsOptions() {
* @returns {number} Randomly generated number.
*/
export function randomInt(min: number, max: number): number {
+ // eslint-disable-next-line sonarjs/pseudo-random
return Math.floor(Math.random() * (max - min + 1)) + min;
}
diff --git a/webpack.common.js b/webpack.common.js
index 3534e683e1..b66f0b7611 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -1,3 +1,4 @@
+const fg = require('fast-glob');
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
@@ -33,9 +34,19 @@ try {
const NODE_MODULES_REGEX = /[\\/]node_modules[\\/]/;
+const THEMES = fg.globSync('themes/**/*.scss', { cwd: path.resolve(__dirname, 'src') });
+const THEMES_BY_ID = THEMES.reduce((acc, theme) => {
+ acc[theme.substring(0, theme.lastIndexOf('/'))] = `./${theme}`;
+ return acc;
+}, {});
+
const config = {
context: path.resolve(__dirname, 'src'),
target: 'browserslist',
+ entry: {
+ 'main.jellyfin': './index.jsx',
+ ...THEMES_BY_ID
+ },
resolve: {
extensions: ['.tsx', '.ts', '.js'],
modules: [
@@ -60,13 +71,14 @@ const config = {
filename: 'index.html',
template: 'index.html',
// Append file hashes to bundle urls for cache busting
- hash: true
+ hash: true,
+ chunks: [
+ 'main.jellyfin',
+ 'serviceworker'
+ ]
}),
new CopyPlugin({
patterns: [
- {
- from: 'themes/**/*.{css,jpg}'
- },
{
from: 'assets/**',
globOptions: {
@@ -107,6 +119,15 @@ const config = {
typescript: {
configFile: path.resolve(__dirname, 'tsconfig.json')
}
+ }),
+ new MiniCssExtractPlugin({
+ filename: pathData => {
+ if (pathData.chunk?.name?.startsWith('themes/')) {
+ return '[name]/theme.css';
+ }
+ return '[name].[contenthash].css';
+ },
+ chunkFilename: '[name].[contenthash].css'
})
],
output: {
@@ -114,6 +135,12 @@ const config = {
pathData.chunk.name === 'serviceworker' ? '[name].js' : '[name].bundle.js'
),
chunkFilename: '[name].[contenthash].chunk.js',
+ assetModuleFilename: pathData => {
+ if (pathData.filename.startsWith('assets/') || pathData.filename.startsWith('themes/')) {
+ return '[path][base][query]';
+ }
+ return '[hash][ext][query]';
+ },
path: path.resolve(__dirname, 'dist'),
publicPath: ''
},
@@ -288,33 +315,46 @@ const config = {
}]
},
{
- test: /\.s[ac]ss$/i,
- use: [
- DEV_MODE ? 'style-loader' : MiniCssExtractPlugin.loader,
- 'css-loader',
+ test: /\.(sa|sc|c)ss$/i,
+ oneOf: [
{
- loader: 'postcss-loader',
- options: {
- postcssOptions: {
- config: path.resolve(__dirname, 'postcss.config.js')
- }
- }
+ // Themes always need to use the MiniCssExtractPlugin since they are loaded directly
+ include: [
+ path.resolve(__dirname, 'src/themes/')
+ ],
+ use: [
+ {
+ loader: MiniCssExtractPlugin.loader,
+ options: {
+ publicPath: '/'
+ }
+ },
+ 'css-loader',
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ config: path.resolve(__dirname, 'postcss.config.js')
+ }
+ }
+ },
+ 'sass-loader'
+ ]
},
- 'sass-loader'
- ]
- },
- {
- test: /\.css$/i,
- use: [
- DEV_MODE ? 'style-loader' : MiniCssExtractPlugin.loader,
- 'css-loader',
{
- loader: 'postcss-loader',
- options: {
- postcssOptions: {
- config: path.resolve(__dirname, 'postcss.config.js')
- }
- }
+ use: [
+ DEV_MODE ? 'style-loader' : MiniCssExtractPlugin.loader,
+ 'css-loader',
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ config: path.resolve(__dirname, 'postcss.config.js')
+ }
+ }
+ },
+ 'sass-loader'
+ ]
}
]
},
@@ -341,10 +381,4 @@ const config = {
}
};
-if (!DEV_MODE) {
- config.plugins.push(new MiniCssExtractPlugin({
- filename: '[name].[contenthash].css'
- }));
-}
-
module.exports = config;
diff --git a/webpack.dev.js b/webpack.dev.js
index a6288a80ce..58c5fbbd68 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -1,11 +1,11 @@
-const common = require('./webpack.common');
const { merge } = require('webpack-merge');
+const common = require('./webpack.common');
+
module.exports = merge(common, {
// In order for live reload to work we must use "web" as the target not "browserslist"
target: process.env.WEBPACK_SERVE ? 'web' : 'browserslist',
mode: 'development',
- entry: { 'main.jellyfin': './index.jsx' },
devtool: 'eval-cheap-module-source-map',
module: {
rules: [
diff --git a/webpack.prod.js b/webpack.prod.js
index 8b2c9aad5e..e45f83f19d 100644
--- a/webpack.prod.js
+++ b/webpack.prod.js
@@ -1,10 +1,11 @@
-const common = require('./webpack.common');
const { merge } = require('webpack-merge');
+const common = require('./webpack.common');
+
module.exports = merge(common, {
mode: 'production',
entry: {
- 'main.jellyfin': './index.jsx',
+ ...common.entry,
'serviceworker': './serviceworker.js'
}
});