1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'master' into master

This commit is contained in:
Jessie Wilson 2025-03-30 01:24:16 -04:00 committed by GitHub
commit bac03437f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
108 changed files with 2005 additions and 1723 deletions

View file

@ -1,6 +1,5 @@
{
"ecmaVersion": "es5",
"modules": "false",
"files": "./dist/**/*.js",
"not": [
"./dist/libraries/pdf.worker.js",

View file

@ -26,15 +26,15 @@ jobs:
show-progress: false
- name: Initialize CodeQL 🛠️
uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
with:
queries: security-and-quality
languages: ${{ matrix.language }}
- name: Autobuild 📦
uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
- name: Perform CodeQL Analysis 🧪
uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
with:
category: '/language:${{matrix.language}}'

View file

@ -98,7 +98,7 @@ export default tseslint.config(
'sonarjs/fixme-tag': 'warn',
'sonarjs/todo-tag': 'off',
'sonarjs/deprecation': 'warn',
'sonarjs/deprecation': 'off',
'sonarjs/no-alphabetical-sort': 'warn',
'sonarjs/no-inverted-boolean-check': 'error',
'sonarjs/no-selector-parameter': 'off',
@ -329,6 +329,7 @@ export default tseslint.config(
}
}
],
'@typescript-eslint/no-deprecated': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error'
}
@ -366,7 +367,6 @@ export default tseslint.config(
rules: {
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'sonarjs/public-static-readonly': 'off',

1418
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,12 +5,12 @@
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@babel/core": "7.26.9",
"@babel/core": "7.26.10",
"@babel/plugin-transform-modules-umd": "7.25.9",
"@babel/preset-env": "7.26.9",
"@babel/preset-react": "7.26.3",
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@eslint/js": "9.22.0",
"@eslint/js": "9.23.0",
"@stylistic/eslint-plugin": "4.2.0",
"@stylistic/stylelint-plugin": "3.1.2",
"@types/dompurify": "3.0.5",
@ -18,13 +18,13 @@
"@types/loadable__component": "5.13.9",
"@types/lodash-es": "4.17.12",
"@types/markdown-it": "14.1.2",
"@types/react": "18.3.18",
"@types/react": "18.3.19",
"@types/react-dom": "18.3.5",
"@types/react-lazy-load-image-component": "1.6.4",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/parser": "8.26.1",
"@typescript-eslint/parser": "8.27.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"@vitest/coverage-v8": "3.0.8",
"@vitest/coverage-v8": "3.0.9",
"autoprefixer": "10.4.21",
"babel-loader": "10.0.0",
"clean-webpack-plugin": "4.0.0",
@ -33,8 +33,8 @@
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"cssnano": "7.0.6",
"es-check": "7.2.1",
"eslint": "9.22.0",
"es-check": "8.0.2",
"eslint": "9.23.0",
"eslint-plugin-compat": "6.0.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.2",
@ -42,6 +42,7 @@
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-sonarjs": "3.0.2",
"expose-loader": "5.0.1",
"fast-glob": "3.3.3",
"fork-ts-checker-webpack-plugin": "9.0.2",
"globals": "16.0.0",
"html-loader": "5.1.0",
@ -52,7 +53,7 @@
"postcss-loader": "8.1.1",
"postcss-preset-env": "10.1.5",
"postcss-scss": "4.0.9",
"sass": "1.85.1",
"sass": "1.86.0",
"sass-loader": "16.0.5",
"source-map-loader": "5.0.0",
"speed-measure-webpack-plugin": "1.5.0",
@ -64,8 +65,8 @@
"stylelint-scss": "6.11.1",
"ts-loader": "9.5.2",
"typescript": "5.8.2",
"typescript-eslint": "8.26.1",
"vitest": "3.0.8",
"typescript-eslint": "8.27.0",
"vitest": "3.0.9",
"webpack": "5.98.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "6.0.1",
@ -76,20 +77,20 @@
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@fontsource/noto-sans": "5.2.5",
"@fontsource/noto-sans": "5.2.6",
"@fontsource/noto-sans-hk": "5.2.5",
"@fontsource/noto-sans-jp": "5.2.5",
"@fontsource/noto-sans-kr": "5.2.5",
"@fontsource/noto-sans-sc": "5.2.5",
"@fontsource/noto-sans-tc": "5.2.5",
"@jellyfin/libass-wasm": "4.2.3",
"@jellyfin/sdk": "0.0.0-unstable.202503230501",
"@mui/icons-material": "6.4.7",
"@mui/material": "6.4.7",
"@mui/x-date-pickers": "7.26.0",
"@jellyfin/sdk": "0.0.0-unstable.202503260501",
"@mui/icons-material": "6.4.8",
"@mui/material": "6.4.8",
"@mui/x-date-pickers": "7.28.0",
"@react-hook/resize-observer": "2.0.2",
"@tanstack/react-query": "5.68.0",
"@tanstack/react-query-devtools": "5.68.0",
"@tanstack/react-query": "5.69.0",
"@tanstack/react-query-devtools": "5.69.0",
"abortcontroller-polyfill": "1.7.8",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
@ -113,7 +114,7 @@
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0",
"material-react-table": "2.13.3",
"material-react-table": "3.2.1",
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"react": "18.3.1",
@ -130,7 +131,7 @@
"whatwg-fetch": "3.6.20"
},
"optionalDependencies": {
"sass-embedded": "1.85.1"
"sass-embedded": "1.86.0"
},
"browserslist": [
"last 2 Firefox versions",

View file

@ -11,6 +11,7 @@ import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
import { useLocale } from 'hooks/useLocale';
@ -101,6 +102,7 @@ export const Component: FC = () => {
</AppBody>
</Box>
</Box>
<ThemeCss dashboard />
</LocalizationProvider>
);
};

View file

@ -148,11 +148,13 @@ const NewTriggerForm: FunctionComponent<IProps> = ({ open, title, onClose, onAdd
fullWidth
defaultValue={''}
type='number'
inputProps={{
min: 1,
step: 0.5
}}
label={globalize.translate('LabelTimeLimitHours')}
slotProps={{
htmlInput: {
min: 1,
step: 0.5
}
}}
/>
</Stack>
</DialogContent>

View file

@ -123,14 +123,16 @@ export const Component = () => {
multiline
minRows={5}
maxRows={5}
InputProps={{
className: 'textarea-mono'
}}
name={BrandingOption.LoginDisclaimer}
label={globalize.translate('LabelLoginDisclaimer')}
helperText={globalize.translate('LabelLoginDisclaimerHelp')}
value={brandingOptions?.LoginDisclaimer}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<TextField
@ -138,14 +140,16 @@ export const Component = () => {
multiline
minRows={5}
maxRows={20}
InputProps={{
className: 'textarea-mono'
}}
name={BrandingOption.CustomCss}
label={globalize.translate('LabelCustomCss')}
helperText={globalize.translate('LabelCustomCssHelp')}
value={brandingOptions?.CustomCss}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<Button

View file

@ -127,12 +127,14 @@ export const Component = () => {
name={'DummyChapterDuration'}
defaultValue={config.DummyChapterDuration}
type='number'
inputProps={{
min: 0,
required: true
}}
label={globalize.translate('LabelDummyChapterDuration')}
helperText={globalize.translate('LabelDummyChapterDurationHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<TextField

View file

@ -81,12 +81,14 @@ export const Component = () => {
name='MinResumePercentage'
type='number'
defaultValue={config?.MinResumePct}
inputProps={{
min: 0,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMinResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
@ -94,12 +96,14 @@ export const Component = () => {
name='MaxResumePercentage'
type='number'
defaultValue={config?.MaxResumePct}
inputProps={{
min: 1,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
@ -107,12 +111,14 @@ export const Component = () => {
name='MinAudiobookResume'
type='number'
defaultValue={config?.MinAudiobookResume}
inputProps={{
min: 0,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
@ -120,12 +126,14 @@ export const Component = () => {
name='MaxAudiobookResume'
type='number'
defaultValue={config?.MaxAudiobookResume}
inputProps={{
min: 1,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
@ -133,11 +141,13 @@ export const Component = () => {
name='MinResumeDuration'
type='number'
defaultValue={config?.MinResumeDurationSeconds}
inputProps={{
min: 0,
required: true
}}
helperText={globalize.translate('LabelMinResumeDurationHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<Button

View file

@ -70,14 +70,16 @@ export const Component = () => {
<TextField
type='number'
inputMode='decimal'
inputProps={{
min: 0,
step: 0.25
}}
name='StreamingBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
helperText={globalize.translate('LabelRemoteClientBitrateLimitHelp')}
defaultValue={defaultConfiguration?.RemoteClientBitrateLimit ? defaultConfiguration?.RemoteClientBitrateLimit / 1e6 : ''}
slotProps={{
htmlInput: {
min: 0,
step: 0.25
}
}}
/>
<Button
type='submit'

View file

@ -158,22 +158,26 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Interval}
inputProps={{
min: 1,
required: true
}}
helperText={globalize.translate('LabelImageIntervalHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelWidthResolutions')}
name='WidthResolutions'
defaultValue={defaultConfig.TrickplayOptions?.WidthResolutions}
inputProps={{
required: true,
pattern: '[0-9,]*'
}}
helperText={globalize.translate('LabelWidthResolutionsHelp')}
slotProps={{
htmlInput: {
required: true,
pattern: '[0-9,]*'
}
}}
/>
<TextField
@ -182,11 +186,13 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileWidth}
inputProps={{
min: 1,
required: true
}}
helperText={globalize.translate('LabelTileWidthHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
@ -195,11 +201,13 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileHeight}
inputProps={{
min: 1,
required: true
}}
helperText={globalize.translate('LabelTileHeightHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
@ -208,12 +216,14 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.JpegQuality}
inputProps={{
min: 1,
max: 100,
required: true
}}
helperText={globalize.translate('LabelJpegQualityHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
@ -222,12 +232,14 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Qscale}
inputProps={{
min: 2,
max: 31,
required: true
}}
helperText={globalize.translate('LabelQscaleHelp')}
slotProps={{
htmlInput: {
min: 2,
max: 31,
required: true
}
}}
/>
<TextField
@ -236,11 +248,13 @@ export const Component = () => {
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.ProcessThreads}
inputProps={{
min: 0,
required: true
}}
helperText={globalize.translate('LabelTrickplayThreadsHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<Button

View file

@ -161,7 +161,6 @@ export const Component = () => {
select
name='UICulture'
label={globalize.translate('LabelPreferredDisplayLanguage')}
FormHelperTextProps={{ component: Stack }}
helperText={(
<>
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
@ -171,6 +170,9 @@ export const Component = () => {
</>
)}
defaultValue={config.UICulture}
slotProps={{
formHelperText: { component: Stack }
}}
>
{languageOptions.map((language) =>
<MenuItem key={language.Name} value={language.Value || ''}>{language.Name}</MenuItem>
@ -185,14 +187,16 @@ export const Component = () => {
helperText={globalize.translate('LabelCachePathHelp')}
value={cachePath}
onChange={onCachePathChange}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showCachePathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
slotProps={{
input: {
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showCachePathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
}
}}
/>
@ -202,14 +206,16 @@ export const Component = () => {
helperText={globalize.translate('LabelMetadataPathHelp')}
value={metadataPath}
onChange={onMetadataPathChange}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showMetadataPathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
slotProps={{
input: {
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showMetadataPathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
}
}}
/>
@ -232,25 +238,29 @@ export const Component = () => {
<TextField
name='LibraryScanFanoutConcurrency'
type='number'
inputProps={{
min: 0,
step: 1
}}
label={globalize.translate('LibraryScanFanoutConcurrency')}
helperText={globalize.translate('LibraryScanFanoutConcurrencyHelp')}
defaultValue={config.LibraryScanFanoutConcurrency || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/>
<TextField
name='ParallelImageEncodingLimit'
type='number'
inputProps={{
min: 0,
step: 1
}}
label={globalize.translate('LabelParallelImageEncodingLimit')}
helperText={globalize.translate('LabelParallelImageEncodingLimitHelp')}
defaultValue={config.ParallelImageEncodingLimit || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/>
<Button type='submit' size='large'>

View file

@ -6,8 +6,10 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
import AppToolbar from './components/AppToolbar';
@ -29,52 +31,56 @@ export const Component = () => {
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<StrictMode>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
<>
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<StrictMode>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
</StrictMode>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
</StrictMode>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
</Box>
<ThemeCss />
<CustomCss />
</>
);
};

View file

@ -146,18 +146,20 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere
<TextField
aria-describedby='display-settings-screensaver-interval-description'
value={values.screensaverInterval}
inputProps={{
inputMode: 'numeric',
max: '3600',
min: '1',
pattern: '[0-9]',
required: true,
step: '1',
type: 'number'
}}
label={globalize.translate('LabelBackdropScreensaverInterval')}
name='screensaverInterval'
onChange={onChange}
slotProps={{
htmlInput: {
inputMode: 'numeric',
max: '3600',
min: '1',
pattern: '[0-9]',
required: true,
step: '1',
type: 'number'
}
}}
/>
<FormHelperText id='display-settings-screensaver-interval-description'>
{globalize.translate('LabelBackdropScreensaverIntervalHelp')}

View file

@ -24,19 +24,21 @@ export function LibraryPreferences({ onChange, values }: Readonly<LibraryPrefere
<FormControl fullWidth>
<TextField
aria-describedby='display-settings-lib-pagesize-description'
inputProps={{
type: 'number',
inputMode: 'numeric',
max: '1000',
min: '0',
pattern: '[0-9]',
required: true,
step: '1'
}}
value={values.libraryPageSize}
label={globalize.translate('LabelLibraryPageSize')}
name='libraryPageSize'
onChange={onChange}
slotProps={{
htmlInput: {
type: 'number',
inputMode: 'numeric',
max: '1000',
min: '0',
pattern: '[0-9]',
required: true,
step: '1'
}
}}
/>
<FormHelperText id='display-settings-lib-pagesize-description'>
{globalize.translate('LabelLibraryPageSizeHelp')}

View file

@ -25,18 +25,20 @@ export function NextUpPreferences({ onChange, values }: Readonly<NextUpPreferenc
<TextField
aria-describedby='display-settings-max-days-next-up-description'
value={values.maxDaysForNextUp}
inputProps={{
type: 'number',
inputMode: 'numeric',
max: '1000',
min: '0',
pattern: '[0-9]',
required: true,
step: '1'
}}
label={globalize.translate('LabelMaxDaysForNextUp')}
name='maxDaysForNextUp'
onChange={onChange}
slotProps={{
htmlInput: {
type: 'number',
inputMode: 'numeric',
max: '1000',
min: '0',
pattern: '[0-9]',
required: true,
step: '1'
}
}}
/>
<FormHelperText id='display-settings-max-days-next-up-description'>
{globalize.translate('LabelMaxDaysForNextUpHelp')}

View file

@ -2,11 +2,17 @@ import React from 'react';
import { Outlet } from 'react-router-dom';
import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ThemeCss from 'components/ThemeCss';
export default function AppLayout() {
return (
<AppBody>
<Outlet />
</AppBody>
<>
<AppBody>
<Outlet />
</AppBody>
<ThemeCss />
<CustomCss />
</>
);
}

View file

@ -0,0 +1,23 @@
import { Api } from '@jellyfin/sdk/lib/api';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { AxiosRequestConfig } from 'axios';
import { QUERY_OPTIONS } from '../constants/queryOptions';
export const fetchItemsByType = async (
api: Api,
userId?: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await getItemsApi(api).getItems(
{
...QUERY_OPTIONS,
userId,
recursive: true,
...params
},
options
);
return response.data;
};

View file

@ -0,0 +1,49 @@
import { Api } from '@jellyfin/sdk';
import { ArtistsApiGetArtistsRequest } from '@jellyfin/sdk/lib/generated-client/api/artists-api';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { isMusic } from '../utils/search';
const fetchArtists = async (
api: Api,
userId: string,
params?: ArtistsApiGetArtistsRequest,
options?: AxiosRequestConfig
) => {
const response = await getArtistsApi(api).getArtists(
{
...QUERY_OPTIONS,
userId,
...params
},
options
);
return response.data;
};
export const useArtistsSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['Search', 'Artists', collectionType, parentId, searchTerm],
queryFn: ({ signal }) => fetchArtists(
api!,
userId!,
{
parentId,
searchTerm
},
{ signal }
),
enabled: !!api && !!userId && (!collectionType || isMusic(collectionType))
});
};

View file

@ -0,0 +1,150 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { addSection, isLivetv } from '../utils/search';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
import { CardShape } from 'utils/card';
import { Section } from '../types';
import { fetchItemsByType } from './fetchItemsByType';
const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | undefined, signal: AbortSignal) => {
const sections: Section[] = [];
// Movies row
const movies = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isMovie: true,
searchTerm
},
{ signal }
).then(moviesData => {
addSection(sections, 'Movies', moviesData.Items, {
...LIVETV_CARD_OPTIONS,
shape: CardShape.PortraitOverflow
});
});
// Episodes row
const episodes = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isMovie: false,
isSeries: true,
isSports: false,
isKids: false,
isNews: false,
searchTerm
},
{ signal }
).then(episodesData => {
addSection(sections, 'Episodes', episodesData.Items, {
...LIVETV_CARD_OPTIONS
});
});
// Sports row
const sports = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isSports: true,
searchTerm
},
{ signal }
).then(sportsData => {
addSection(sections, 'Sports', sportsData.Items, {
...LIVETV_CARD_OPTIONS
});
});
// Kids row
const kids = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isKids: true,
searchTerm
},
{ signal }
).then(kidsData => {
addSection(sections, 'Kids', kidsData.Items, {
...LIVETV_CARD_OPTIONS
});
});
// News row
const news = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isNews: true,
searchTerm
},
{ signal }
).then(newsData => {
addSection(sections, 'News', newsData.Items, {
...LIVETV_CARD_OPTIONS
});
});
// Programs row
const programs = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
isMovie: false,
isSeries: false,
isSports: false,
isKids: false,
isNews: false,
searchTerm
},
{ signal }
).then(programsData => {
addSection(sections, 'Programs', programsData.Items, {
...LIVETV_CARD_OPTIONS
});
});
// Channels row
const channels = fetchItemsByType(
api,
userId,
{
includeItemTypes: [ BaseItemKind.TvChannel ],
searchTerm
},
{ signal }
).then(channelsData => {
addSection(sections, 'Channels', channelsData.Items);
});
return Promise.all([ movies, episodes, sports, kids, news, programs, channels ]).then(() => sections);
};
export const useLiveTvSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['Search', 'LiveTv', collectionType, parentId, searchTerm],
queryFn: ({ signal }) =>
fetchLiveTv(api!, userId!, searchTerm, signal),
enabled: !!api && !!userId && !!collectionType && !!isLivetv(collectionType)
});
};

View file

@ -0,0 +1,50 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { isMovies, isTVShows } from '../utils/search';
import { PersonsApiGetPersonsRequest } from '@jellyfin/sdk/lib/generated-client/api/persons-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
const fetchPeople = async (
api: Api,
userId: string,
params?: PersonsApiGetPersonsRequest,
options?: AxiosRequestConfig
) => {
const response = await getPersonsApi(api).getPersons(
{
...QUERY_OPTIONS,
userId,
...params
},
options
);
return response.data;
};
export const usePeopleSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
const isPeopleEnabled = (!collectionType || isMovies(collectionType) || isTVShows(collectionType));
return useQuery({
queryKey: ['Search', 'People', collectionType, parentId, searchTerm],
queryFn: ({ signal }) => fetchPeople(
api!,
userId!,
{
searchTerm
},
{ signal }
),
enabled: !!api && !!userId && isPeopleEnabled
});
};

View file

@ -0,0 +1,50 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
import { fetchItemsByType } from './fetchItemsByType';
const fetchPrograms = async (
api: Api,
userId: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
...params
},
options
);
return response;
};
export const useProgramsSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['Search', 'Programs', collectionType, parentId, searchTerm],
queryFn: ({ signal }) => fetchPrograms(
api!,
userId!,
{
parentId,
searchTerm
},
{ signal }
),
enabled: !!api && !!userId && !collectionType
});
};

View file

@ -0,0 +1,98 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { useApi } from '../../../../../hooks/useApi';
import { addSection, getCardOptionsFromType, getItemTypesFromCollectionType, getTitleFromType, isLivetv, isMovies, isMusic, isTVShows, sortSections } from '../utils/search';
import { useArtistsSearch } from './useArtistsSearch';
import { usePeopleSearch } from './usePeopleSearch';
import { useVideoSearch } from './useVideoSearch';
import { Section } from '../types';
import { useLiveTvSearch } from './useLiveTvSearch';
import { fetchItemsByType } from './fetchItemsByType';
import { useProgramsSearch } from './useProgramsSearch';
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
export const useSearchItems = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { data: artists, isPending: isArtistsPending } = useArtistsSearch(parentId, collectionType, searchTerm);
const { data: people, isPending: isPeoplePending } = usePeopleSearch(parentId, collectionType, searchTerm);
const { data: videos, isPending: isVideosPending } = useVideoSearch(parentId, collectionType, searchTerm);
const { data: programs, isPending: isProgramsPending } = useProgramsSearch(parentId, collectionType, searchTerm);
const { data: liveTvSections, isPending: isLiveTvPending } = useLiveTvSearch(parentId, collectionType, searchTerm);
const { api, user } = useApi();
const userId = user?.Id;
const isArtistsEnabled = !isArtistsPending || (collectionType && !isMusic(collectionType));
const isPeopleEnabled = !isPeoplePending || (collectionType && !isMovies(collectionType) && !isTVShows(collectionType));
const isVideosEnabled = !isVideosPending || collectionType;
const isProgramsEnabled = !isProgramsPending || collectionType;
const isLiveTvEnabled = !isLiveTvPending || !collectionType || !isLivetv(collectionType);
return useQuery({
queryKey: ['Search', 'Items', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) => {
if (liveTvSections && collectionType && isLivetv(collectionType)) {
return sortSections(liveTvSections);
}
const sections: Section[] = [];
addSection(sections, 'Artists', artists?.Items, {
coverImage: true
});
addSection(sections, 'Programs', programs?.Items, {
...LIVETV_CARD_OPTIONS
});
addSection(sections, 'People', people?.Items, {
coverImage: true
});
addSection(sections, 'HeaderVideos', videos?.Items, {
showParentTitle: true
});
const itemTypes: BaseItemKind[] = getItemTypesFromCollectionType(collectionType);
const searchData = await fetchItemsByType(
api!,
userId,
{
includeItemTypes: itemTypes,
parentId,
searchTerm,
limit: 800
},
{ signal }
);
if (searchData.Items) {
for (const itemType of itemTypes) {
const items: BaseItemDto[] = [];
for (const searchItem of searchData.Items) {
if (searchItem.Type === itemType) {
items.push(searchItem);
}
}
addSection(sections, getTitleFromType(itemType), items, getCardOptionsFromType(itemType));
}
}
return sortSections(sections);
},
enabled: (
!!api
&& !!userId
&& !!isArtistsEnabled
&& !!isPeopleEnabled
&& !!isVideosEnabled
&& !!isLiveTvEnabled
&& !!isProgramsEnabled
)
});
};

View file

@ -4,20 +4,17 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from '../useApi';
import { useApi } from 'hooks/useApi';
const fetchGetItems = async (
api?: Api,
userId?: string,
api: Api,
userId: string,
parentId?: string,
options?: AxiosRequestConfig
) => {
if (!api) throw new Error('No API instance available');
if (!userId) throw new Error('No User ID provided');
const response = await getItemsApi(api).getItems(
{
userId: userId,
userId,
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
includeItemTypes: [
BaseItemKind.Movie,
@ -28,7 +25,7 @@ const fetchGetItems = async (
recursive: true,
imageTypeLimit: 0,
enableImages: false,
parentId: parentId,
parentId,
enableTotalRecordCount: false
},
options
@ -43,7 +40,8 @@ export const useSearchSuggestions = (parentId?: string) => {
return useQuery({
queryKey: ['SearchSuggestions', { parentId }],
queryFn: ({ signal }) =>
fetchGetItems(api, userId, parentId, { signal }),
fetchGetItems(api!, userId!, parentId, { signal }),
refetchOnWindowFocus: false,
enabled: !!api && !!userId
});
};

View file

@ -0,0 +1,57 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
const fetchVideos = async (
api: Api,
userId: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await getItemsApi(api).getItems(
{
...QUERY_OPTIONS,
userId,
recursive: true,
mediaTypes: [MediaType.Video],
excludeItemTypes: [
BaseItemKind.Movie,
BaseItemKind.Episode,
BaseItemKind.TvChannel
],
...params
},
options
);
return response.data;
};
export const useVideoSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['Search', 'Video', collectionType, parentId, searchTerm],
queryFn: ({ signal }) => fetchVideos(
api!,
userId!,
{
parentId,
searchTerm
},
{ signal }
),
enabled: !!api && !!userId && !collectionType
});
};

View file

@ -1,11 +1,11 @@
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
import AlphaPicker from 'components/alphaPicker/AlphaPickerComponent';
import Input from 'elements/emby-input/Input';
import globalize from '../../lib/globalize';
import layoutManager from '../layoutManager';
import browser from '../../scripts/browser';
import globalize from 'lib/globalize';
import layoutManager from 'components/layoutManager';
import browser from 'scripts/browser';
import 'material-design-icons-iconfont';
import '../../styles/flexstyles.scss';
import 'styles/flexstyles.scss';
import './searchfields.scss';
interface SearchFieldsProps {

View file

@ -1,13 +1,16 @@
import React, { type FC } from 'react';
import { Section, useSearchItems } from 'hooks/searchHook';
import globalize from '../../lib/globalize';
import Loading from '../loading/LoadingComponent';
import { useSearchItems } from '../api/useSearchItems';
import globalize from 'lib/globalize';
import Loading from 'components/loading/LoadingComponent';
import SearchResultsRow from './SearchResultsRow';
import { CardShape } from 'utils/card';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { Section } from '../types';
import { Link } from 'react-router-dom';
interface SearchResultsProps {
parentId?: string;
collectionType?: string;
collectionType?: CollectionType;
query?: string;
}
@ -19,14 +22,22 @@ const SearchResults: FC<SearchResultsProps> = ({
collectionType,
query
}) => {
const { isLoading, data } = useSearchItems(parentId, collectionType, query);
const { data, isPending } = useSearchItems(parentId, collectionType, query?.trim());
if (isLoading) return <Loading />;
if (isPending) return <Loading />;
if (!data?.length) {
return (
<div className='noItemsMessage centerMessage'>
{globalize.translate('SearchResultsEmpty', query)}
{collectionType && (
<div>
<Link
className='emby-button'
to={`/search.html?query=${encodeURIComponent(query || '')}`}
>{globalize.translate('RetryWithGlobalSearch')}</Link>
</div>
)}
</div>
);
}
@ -51,7 +62,7 @@ const SearchResults: FC<SearchResultsProps> = ({
};
return (
<div className={'searchResults, padded-top, padded-bottom-page'}>
<div className={'searchResults padded-top padded-bottom-page'}>
{data.map((section, index) => renderSection(section, index))}
</div>
);

View file

@ -1,10 +1,10 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { type FC, useEffect, useRef } from 'react';
import cardBuilder from '../cardbuilder/cardBuilder';
import cardBuilder from 'components/cardbuilder/cardBuilder';
import type { CardOptions } from 'types/cardOptions';
import '../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import 'elements/emby-scroller/emby-scroller';
import 'elements/emby-itemscontainer/emby-itemscontainer';
// There seems to be some compatibility issues here between
// React and our legacy web components, so we need to inject

View file

@ -1,21 +1,21 @@
import React, { FunctionComponent } from 'react';
import Loading from 'components/loading/LoadingComponent';
import { appRouter } from '../router/appRouter';
import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions';
import { appRouter } from 'components/router/appRouter';
import { useSearchSuggestions } from '../api/useSearchSuggestions';
import globalize from 'lib/globalize';
import LinkButton from '../../elements/emby-button/LinkButton';
import LinkButton from 'elements/emby-button/LinkButton';
import '../../elements/emby-button/emby-button';
import 'elements/emby-button/emby-button';
type SearchSuggestionsProps = {
parentId?: string | null;
};
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => {
const { isLoading, data: suggestions } = useSearchSuggestions(parentId || undefined);
const { data: suggestions, isPending } = useSearchSuggestions(parentId || undefined);
if (isLoading) return <Loading />;
if (isPending) return <Loading />;
return (
<div

View file

@ -0,0 +1,11 @@
export const LIVETV_CARD_OPTIONS = {
preferThumb: true,
inheritThumb: false,
showParentTitleOrTitle: true,
showTitle: false,
coverImage: true,
overlayMoreButton: true,
showAirTime: true,
showAirDateTime: true,
showChannelName: true
};

View file

@ -0,0 +1,12 @@
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
export const QUERY_OPTIONS = {
limit: 100,
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.CanDelete,
ItemFields.MediaSourceCount
],
enableTotalRecordCount: false,
imageTypeLimit: 1
};

View file

@ -0,0 +1,18 @@
export const SEARCH_SECTIONS_SORT_ORDER = [
'Movies',
'Shows',
'Episodes',
'People',
'Playlists',
'Artists',
'Albums',
'Songs',
'HeaderVideos',
'Programs',
'Channels',
'HeaderPhotoAlbums',
'Photos',
'HeaderAudioBooks',
'Books',
'Collections'
];

View file

@ -0,0 +1,8 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { CardOptions } from 'types/cardOptions';
export interface Section {
title: string
items: BaseItemDto[];
cardOptions?: CardOptions;
};

View file

@ -0,0 +1,141 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { CardShape } from 'utils/card';
import { Section } from '../types';
import { CardOptions } from 'types/cardOptions';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
import { SEARCH_SECTIONS_SORT_ORDER } from '../constants/sectionSortOrder';
export const isMovies = (collectionType: string) =>
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
];
}
}

View file

@ -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'
>
<SearchFields query={query} onSearch={setQuery} />
{!query ? (
{!debouncedQuery ? (
<SearchSuggestions
parentId={parentIdQuery}
/>

View file

@ -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 && (
<style>
{brandingOptions.CustomCss}
</style>
)}
{userCustomCss && (
<style>
{userCustomCss}
</style>
)}
</>
);
};
export default CustomCss;

View file

@ -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;
};

View file

@ -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<ThemeCssProps> = ({
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 (
<link
rel='stylesheet'
type='text/css'
href={themeUrl}
/>
);
};
export default ThemeCss;

View file

@ -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 {

View file

@ -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 {

View file

@ -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++) {

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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(

View file

@ -25,7 +25,6 @@ const Lists: FC<ListsProps> = ({ items = [], listOptions = {} }) => {
const renderListItem = (item: ItemDto, index: number) => {
return (
<List
// eslint-disable-next-line react/no-array-index-key
key={`${item.Id}-${index}`}
index={index}
item={item}

View file

@ -23,6 +23,7 @@ import toast from '../toast/toast';
import confirm from '../confirm/confirm';
import template from './mediaLibraryEditor.template.html';
// eslint-disable-next-line sonarjs/no-invariant-returns
function onEditLibrary() {
if (isCreating) {
return false;

View file

@ -60,7 +60,7 @@ function getProgramInfoHtml(item, options) {
miscInfo.push(text);
} catch (e) {
console.error('error parsing date:', item.StartDate);
console.error('error parsing date:', item.StartDate, e);
}
}
@ -140,7 +140,7 @@ export function getMediaInfoHtml(item, options = {}) {
text = datetime.toLocaleDateString(date);
miscInfo.push(text);
} catch (e) {
console.error('error parsing date:', item.PremiereDate);
console.error('error parsing date:', item.PremiereDate, e);
}
}
@ -170,7 +170,7 @@ export function getMediaInfoHtml(item, options = {}) {
miscInfo.push(text);
}
} catch (e) {
console.error('error parsing date:', item.StartDate);
console.error('error parsing date:', item.StartDate, e);
}
}
@ -188,7 +188,7 @@ export function getMediaInfoHtml(item, options = {}) {
text += ` - ${endYear}`;
}
} catch (e) {
console.error('error parsing date:', item.EndDate);
console.error('error parsing date:', item.EndDate, e);
}
}
@ -238,7 +238,7 @@ export function getMediaInfoHtml(item, options = {}) {
text = globalize.translate('OriginalAirDateValue', datetime.toLocaleDateString(date));
miscInfo.push(text);
} catch (e) {
console.error('error parsing date:', program.PremiereDate);
console.error('error parsing date:', program.PremiereDate, e);
}
} else if (program.ProductionYear && options.year !== false ) {
miscInfo.push(program.ProductionYear);
@ -255,7 +255,7 @@ export function getMediaInfoHtml(item, options = {}) {
text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
miscInfo.push(text);
} catch (e) {
console.error('error parsing date:', item.PremiereDate);
console.error('error parsing date:', item.PremiereDate, e);
}
}
}

View file

@ -821,7 +821,7 @@ function fillItemInfo(context, item, parentalRatingOptions) {
date = datetime.parseISO8601Date(item.DateCreated, true);
context.querySelector('#txtDateAdded').value = date.toISOString().slice(0, 10);
} catch (e) {
} catch {
context.querySelector('#txtDateAdded').value = '';
}
} else {
@ -833,7 +833,7 @@ function fillItemInfo(context, item, parentalRatingOptions) {
date = datetime.parseISO8601Date(item.PremiereDate, true);
context.querySelector('#txtPremiereDate').value = date.toISOString().slice(0, 10);
} catch (e) {
} catch {
context.querySelector('#txtPremiereDate').value = '';
}
} else {
@ -845,7 +845,7 @@ function fillItemInfo(context, item, parentalRatingOptions) {
date = datetime.parseISO8601Date(item.EndDate, true);
context.querySelector('#txtEndDate').value = date.toISOString().slice(0, 10);
} catch (e) {
} catch {
context.querySelector('#txtEndDate').value = '';
}
} else {

View file

@ -13,7 +13,6 @@ function onOneDocumentClick() {
// don't request notification permissions if they're already granted or denied
if (window.Notification && window.Notification.permission === 'default') {
/* eslint-disable-next-line compat/compat */
Notification.requestPermission();
}
}

View file

@ -17,7 +17,6 @@ Events.on(playbackManager, 'playbackstart', function (e, player) {
const isLocalVideo = player.isLocalPlayer && !player.isExternalPlayer && playbackManager.isPlayingVideo(player);
if (isLocalVideo && layoutManager.mobile) {
/* eslint-disable-next-line compat/compat */
const lockOrientation = window.screen.lockOrientation || window.screen.mozLockOrientation || window.screen.msLockOrientation || (window.screen.orientation?.lock);
if (lockOrientation) {
@ -38,7 +37,6 @@ Events.on(playbackManager, 'playbackstart', function (e, player) {
Events.on(playbackManager, 'playbackstop', function (e, playbackStopInfo) {
if (orientationLocked && !playbackStopInfo.nextMediaType) {
/* eslint-disable-next-line compat/compat */
const unlockOrientation = window.screen.unlockOrientation || window.screen.mozUnlockOrientation || window.screen.msUnlockOrientation || (window.screen.orientation?.unlock);
if (unlockOrientation) {

View file

@ -265,7 +265,7 @@ export default function (view) {
document.addEventListener('keydown', onKeyDown);
try {
onLoad();
} catch (e) {
} catch {
appRouter.goHome();
}
});

View file

@ -713,7 +713,7 @@ export default function (view) {
}, state);
}
} catch (e) {
console.error('error parsing date: ' + program.EndDate);
console.error('error parsing date: ' + program.EndDate, e);
}
}
}
@ -1352,7 +1352,7 @@ export default function (view) {
case 'GamepadDPadLeft':
case 'GamepadLeftThumbstickLeft':
// Ignores gamepad events that are always triggered, even when not focused.
if (document.hasFocus()) { /* eslint-disable-line compat/compat */
if (document.hasFocus()) {
playbackManager.rewind(currentPlayer);
showOsd(btnRewind);
}
@ -1361,7 +1361,7 @@ export default function (view) {
case 'GamepadDPadRight':
case 'GamepadLeftThumbstickRight':
// Ignores gamepad events that are always triggered, even when not focused.
if (document.hasFocus()) { /* eslint-disable-line compat/compat */
if (document.hasFocus()) {
playbackManager.fastForward(currentPlayer);
showOsd(btnFastForward);
}
@ -1712,7 +1712,7 @@ export default function (view) {
if (browser.firefox || browser.edge) {
dom.addEventListener(document, 'click', onClickCapture, { capture: true });
}
} catch (e) {
} catch {
setBackdropTransparency(TRANSPARENCY_LEVEL.None); // reset state set in viewbeforeshow
appRouter.goHome();
}

View file

@ -61,7 +61,7 @@ function renderUpcoming(elem, items) {
day: 'numeric'
});
} catch (err) {
console.error('error parsing timestamp for upcoming tv shows');
console.error('error parsing timestamp for upcoming tv shows', err);
}
}

View file

@ -1,2 +0,0 @@
export * from './useSearchItems';
export * from './useSearchSuggestions';

View file

@ -1,509 +0,0 @@
import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk';
import type {
ArtistsApiGetArtistsRequest,
BaseItemDto,
ItemsApiGetItemsRequest,
PersonsApiGetPersonsRequest
} from '@jellyfin/sdk/lib/generated-client';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from '../useApi';
import type { CardOptions } from 'types/cardOptions';
import { CardShape } from 'utils/card';
const QUERY_OPTIONS = {
limit: 100,
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.CanDelete,
ItemFields.MediaSourceCount
],
enableTotalRecordCount: false,
imageTypeLimit: 1
};
const fetchItemsByType = async (
api: Api,
userId?: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
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
});
};

View file

@ -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<UserSettings>({});
const UserSettingsContext = createContext<UserSettings>({
disableCustomCss: false
});
export const useUserSettings = () => useContext(UserSettingsContext);
export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const [ customCss, setCustomCss ] = useState<string>();
const [ disableCustomCss, setDisableCustomCss ] = useState(false);
const [ theme, setTheme ] = useState<string>();
const [ dashboardTheme, setDashboardTheme ] = useState<string>();
const [ dateTimeLocale, setDateTimeLocale ] = useState<string>();
@ -36,14 +45,25 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children
const { user } = useApi();
const context = useMemo<UserSettings>(() => ({
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());

View file

@ -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() {

View file

@ -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');
}

View file

@ -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)) {

View file

@ -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.

View file

@ -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;

View file

@ -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
}
}

View file

@ -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';

View file

@ -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);

View file

@ -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
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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);
}

View file

@ -31,7 +31,7 @@ function getScreensaverPlugin(isLoggedIn) {
let option;
try {
option = userSettings.get('screensaver', false);
} catch (err) {
} catch {
option = isLoggedIn ? 'backdropscreensaver' : 'logoscreensaver';
}

View file

@ -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
};

View file

@ -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": "إجراءات قطاع الوسائط"
}

View file

@ -1955,5 +1955,9 @@
"LabelMediaSegmentProviders": "Пастаўшчыкі сегментаў медыяфайлаў",
"MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.",
"MediaSegmentType.Commercial": "Рэклама",
"MediaSegmentAction.None": "Няма"
"MediaSegmentAction.None": "Няма",
"HeaderNextVideo": "Наступнае відэа",
"HeaderPageNotFound": "Старонка не знойдзена",
"LabelSaveTrickplayLocally": "Захаваць выявы trickplay побач з медыяфайламі",
"LabelSaveTrickplayLocallyHelp": "Захаванне выяў trickplay у тэчках з медыяфайламі дазволіць размясціць іх побач з вашымі медыяфайламі для зручнай міграцыі і доступу."
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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": "Αποτυχία φόρτωσης σελίδας ρυθμίσεων"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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."
}

View file

@ -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 dapplication FFmpeg prenant en charge laccélération OpenCL est requis.",
"LabelParentNumber": "Numéro parent",
"LabelParallelImageEncodingLimitHelp": "Nombre maximal dencodages dimages qui peuvent être exécutés en parallèle. Une valeur de 0 entraînera une sélection automatique dune limite selon le nombre de coeurs de votre système.",
"LabelParallelImageEncodingLimitHelp": "Nombre maximal dencodages dimages qui peuvent être exécutés en parallèle. Une valeur vide entraînera une sélection automatique dune 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"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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."
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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",

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -2012,5 +2012,7 @@
"MetadataImagesLoadError": "Не удалось загрузить настройки метаданных",
"HeaderPageNotFound": "Станица не найдена",
"PageNotFound": "Это не та страница, которую вы искали.",
"SettingsPageLoadError": "Не удалось загрузить страницу параметров"
"SettingsPageLoadError": "Не удалось загрузить страницу параметров",
"RetryWithGlobalSearch": "Повторите попытку с помощью глобального поиска",
"StreamCountExceedsLimit": "Количество потоков превышает предельное значение"
}

View file

@ -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"
}

View file

@ -2009,5 +2009,7 @@
"MetadataNfoLoadError": "Не вдалося завантажити налаштування метаданих NFO",
"HeaderPageNotFound": "Сторінку не знайдено",
"PageNotFound": "Це не та сторінка, яку ви шукаєте.",
"SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань"
"SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань",
"StreamCountExceedsLimit": "Кількість потоків перевищує ліміт",
"RetryWithGlobalSearch": "Повторити спробу з глобальним пошуком"
}

View file

@ -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"
}

Some files were not shown because too many files have changed in this diff Show more