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", "ecmaVersion": "es5",
"modules": "false",
"files": "./dist/**/*.js", "files": "./dist/**/*.js",
"not": [ "not": [
"./dist/libraries/pdf.worker.js", "./dist/libraries/pdf.worker.js",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -161,7 +161,6 @@ export const Component = () => {
select select
name='UICulture' name='UICulture'
label={globalize.translate('LabelPreferredDisplayLanguage')} label={globalize.translate('LabelPreferredDisplayLanguage')}
FormHelperTextProps={{ component: Stack }}
helperText={( helperText={(
<> <>
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span> <span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
@ -171,6 +170,9 @@ export const Component = () => {
</> </>
)} )}
defaultValue={config.UICulture} defaultValue={config.UICulture}
slotProps={{
formHelperText: { component: Stack }
}}
> >
{languageOptions.map((language) => {languageOptions.map((language) =>
<MenuItem key={language.Name} value={language.Value || ''}>{language.Name}</MenuItem> <MenuItem key={language.Name} value={language.Value || ''}>{language.Name}</MenuItem>
@ -185,7 +187,8 @@ export const Component = () => {
helperText={globalize.translate('LabelCachePathHelp')} helperText={globalize.translate('LabelCachePathHelp')}
value={cachePath} value={cachePath}
onChange={onCachePathChange} onChange={onCachePathChange}
InputProps={{ slotProps={{
input: {
endAdornment: ( endAdornment: (
<InputAdornment position='end'> <InputAdornment position='end'>
<IconButton edge='end' onClick={showCachePathPicker}> <IconButton edge='end' onClick={showCachePathPicker}>
@ -193,6 +196,7 @@ export const Component = () => {
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
) )
}
}} }}
/> />
@ -202,7 +206,8 @@ export const Component = () => {
helperText={globalize.translate('LabelMetadataPathHelp')} helperText={globalize.translate('LabelMetadataPathHelp')}
value={metadataPath} value={metadataPath}
onChange={onMetadataPathChange} onChange={onMetadataPathChange}
InputProps={{ slotProps={{
input: {
endAdornment: ( endAdornment: (
<InputAdornment position='end'> <InputAdornment position='end'>
<IconButton edge='end' onClick={showMetadataPathPicker}> <IconButton edge='end' onClick={showMetadataPathPicker}>
@ -210,6 +215,7 @@ export const Component = () => {
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
) )
}
}} }}
/> />
@ -232,25 +238,29 @@ export const Component = () => {
<TextField <TextField
name='LibraryScanFanoutConcurrency' name='LibraryScanFanoutConcurrency'
type='number' type='number'
inputProps={{
min: 0,
step: 1
}}
label={globalize.translate('LibraryScanFanoutConcurrency')} label={globalize.translate('LibraryScanFanoutConcurrency')}
helperText={globalize.translate('LibraryScanFanoutConcurrencyHelp')} helperText={globalize.translate('LibraryScanFanoutConcurrencyHelp')}
defaultValue={config.LibraryScanFanoutConcurrency || ''} defaultValue={config.LibraryScanFanoutConcurrency || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/> />
<TextField <TextField
name='ParallelImageEncodingLimit' name='ParallelImageEncodingLimit'
type='number' type='number'
inputProps={{
min: 0,
step: 1
}}
label={globalize.translate('LabelParallelImageEncodingLimit')} label={globalize.translate('LabelParallelImageEncodingLimit')}
helperText={globalize.translate('LabelParallelImageEncodingLimitHelp')} helperText={globalize.translate('LabelParallelImageEncodingLimitHelp')}
defaultValue={config.ParallelImageEncodingLimit || ''} defaultValue={config.ParallelImageEncodingLimit || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/> />
<Button type='submit' size='large'> <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 { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody'; import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ElevationScroll from 'components/ElevationScroll'; import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer'; import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import AppToolbar from './components/AppToolbar'; import AppToolbar from './components/AppToolbar';
@ -29,6 +31,7 @@ export const Component = () => {
}, [ isDrawerActive, setIsDrawerActive ]); }, [ isDrawerActive, setIsDrawerActive ]);
return ( return (
<>
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}> <Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<StrictMode> <StrictMode>
<ElevationScroll elevate={false}> <ElevationScroll elevate={false}>
@ -76,5 +79,8 @@ export const Component = () => {
</AppBody> </AppBody>
</Box> </Box>
</Box> </Box>
<ThemeCss />
<CustomCss />
</>
); );
}; };

View file

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

View file

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

View file

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

View file

@ -2,11 +2,17 @@ import React from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import AppBody from 'components/AppBody'; import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ThemeCss from 'components/ThemeCss';
export default function AppLayout() { export default function AppLayout() {
return ( return (
<>
<AppBody> <AppBody>
<Outlet /> <Outlet />
</AppBody> </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 { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from '../useApi'; import { useApi } from 'hooks/useApi';
const fetchGetItems = async ( const fetchGetItems = async (
api?: Api, api: Api,
userId?: string, userId: string,
parentId?: string, parentId?: string,
options?: AxiosRequestConfig 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( const response = await getItemsApi(api).getItems(
{ {
userId: userId, userId,
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random], sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
includeItemTypes: [ includeItemTypes: [
BaseItemKind.Movie, BaseItemKind.Movie,
@ -28,7 +25,7 @@ const fetchGetItems = async (
recursive: true, recursive: true,
imageTypeLimit: 0, imageTypeLimit: 0,
enableImages: false, enableImages: false,
parentId: parentId, parentId,
enableTotalRecordCount: false enableTotalRecordCount: false
}, },
options options
@ -43,7 +40,8 @@ export const useSearchSuggestions = (parentId?: string) => {
return useQuery({ return useQuery({
queryKey: ['SearchSuggestions', { parentId }], queryKey: ['SearchSuggestions', { parentId }],
queryFn: ({ signal }) => queryFn: ({ signal }) =>
fetchGetItems(api, userId, parentId, { signal }), fetchGetItems(api!, userId!, parentId, { signal }),
refetchOnWindowFocus: false,
enabled: !!api && !!userId 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 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 Input from 'elements/emby-input/Input';
import globalize from '../../lib/globalize'; import globalize from 'lib/globalize';
import layoutManager from '../layoutManager'; import layoutManager from 'components/layoutManager';
import browser from '../../scripts/browser'; import browser from 'scripts/browser';
import 'material-design-icons-iconfont'; import 'material-design-icons-iconfont';
import '../../styles/flexstyles.scss'; import 'styles/flexstyles.scss';
import './searchfields.scss'; import './searchfields.scss';
interface SearchFieldsProps { interface SearchFieldsProps {

View file

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

View file

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

View file

@ -1,21 +1,21 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import { appRouter } from '../router/appRouter'; import { appRouter } from 'components/router/appRouter';
import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions'; import { useSearchSuggestions } from '../api/useSearchSuggestions';
import globalize from 'lib/globalize'; 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 = { type SearchSuggestionsProps = {
parentId?: string | null; parentId?: string | null;
}; };
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => { 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 ( return (
<div <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 { usePrevious } from 'hooks/usePrevious';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import Page from 'components/Page'; import Page from 'components/Page';
import SearchFields from 'components/search/SearchFields'; import SearchFields from 'apps/stable/features/search/components/SearchFields';
import SearchSuggestions from 'components/search/SearchSuggestions'; import SearchSuggestions from 'apps/stable/features/search/components/SearchSuggestions';
import SearchResults from 'components/search/SearchResults'; 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 COLLECTION_TYPE_PARAM = 'collectionType';
const PARENT_ID_PARAM = 'parentId'; const PARENT_ID_PARAM = 'parentId';
@ -15,7 +16,7 @@ const QUERY_PARAM = 'query';
const Search: FC = () => { const Search: FC = () => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined; 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 urlQuery = searchParams.get(QUERY_PARAM) || '';
const [query, setQuery] = useState(urlQuery); const [query, setQuery] = useState(urlQuery);
const prevQuery = usePrevious(query, ''); const prevQuery = usePrevious(query, '');
@ -50,7 +51,7 @@ const Search: FC = () => {
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage' className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
> >
<SearchFields query={query} onSearch={setQuery} /> <SearchFields query={query} onSearch={setQuery} />
{!query ? ( {!debouncedQuery ? (
<SearchSuggestions <SearchSuggestions
parentId={parentIdQuery} 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 // 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 { Api } from '@jellyfin/sdk';
import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions'; import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions';
import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient'; import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient';
@ -18,7 +18,6 @@ const normalizeImageOptions = options => {
}; };
const getMaxBandwidth = () => { const getMaxBandwidth = () => {
/* eslint-disable compat/compat */
if (navigator.connection) { if (navigator.connection) {
let max = navigator.connection.downlinkMax; let max = navigator.connection.downlinkMax;
if (max && max > 0 && max < Number.POSITIVE_INFINITY) { if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
@ -28,7 +27,6 @@ const getMaxBandwidth = () => {
return parseInt(max, 10); return parseInt(max, 10);
} }
} }
/* eslint-enable compat/compat */
return null; 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 hidden;
let visibilityChange; let visibilityChange;
if (typeof document.hidden !== 'undefined') { /* eslint-disable-line compat/compat */ if (typeof document.hidden !== 'undefined') {
hidden = 'hidden'; hidden = 'hidden';
visibilityChange = 'visibilitychange'; visibilityChange = 'visibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') { } else if (typeof document.webkitHidden !== 'undefined') {
@ -461,7 +461,6 @@ if (typeof document.hidden !== 'undefined') { /* eslint-disable-line compat/comp
} }
document.addEventListener(visibilityChange, function () { document.addEventListener(visibilityChange, function () {
/* eslint-disable-next-line compat/compat */
if (document[hidden]) { if (document[hidden]) {
onAppHidden(); onAppHidden();
} else { } else {

View file

@ -484,7 +484,7 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) {
airTimeText += ' - ' + datetime.getDisplayTime(date); airTimeText += ' - ' + datetime.getDisplayTime(date);
} }
} catch (e) { } 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), datetime.parseISO8601Date(item.PremiereDate),
{ weekday: 'long', month: 'long', day: 'numeric' } { weekday: 'long', month: 'long', day: 'numeric' }
)); ));
} catch (err) { } catch {
lines.push(''); lines.push('');
} }
} else { } else {

View file

@ -678,6 +678,7 @@ describe('getDefaultBackgroundClass', () => {
}); });
test('randomization string provided', () => { test('randomization string provided', () => {
// eslint-disable-next-line sonarjs/pseudo-random
const generateRandomString = (stringLength: number): string => (Math.random() + 1).toString(36).substring(stringLength); const generateRandomString = (stringLength: number): string => (Math.random() + 1).toString(36).substring(stringLength);
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {

View file

@ -14,6 +14,7 @@ function merge(resultItems, queryItems, delimiter) {
if (!queryItems) { if (!queryItems) {
return resultItems; return resultItems;
} }
// eslint-disable-next-line sonarjs/no-alphabetical-sort
return union(resultItems, queryItems.split(delimiter)).sort(); return union(resultItems, queryItems.split(delimiter)).sort();
} }

View file

@ -358,7 +358,7 @@ function Guide(options) {
if ((typeof date).toString().toLowerCase() === 'string') { if ((typeof date).toString().toLowerCase() === 'string') {
try { try {
date = datetime.parseISO8601Date(date, { toLocal: true }); date = datetime.parseISO8601Date(date, { toLocal: true });
} catch (err) { } catch {
return date; return date;
} }
} }
@ -392,7 +392,7 @@ function Guide(options) {
try { try {
program.StartDateLocal = datetime.parseISO8601Date(program.StartDate, { toLocal: true }); program.StartDateLocal = datetime.parseISO8601Date(program.StartDate, { toLocal: true });
} catch (err) { } 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 { try {
program.EndDateLocal = datetime.parseISO8601Date(program.EndDate, { toLocal: true }); program.EndDateLocal = datetime.parseISO8601Date(program.EndDate, { toLocal: true });
} catch (err) { } 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); reader.readAsDataURL(file);
} }
// eslint-disable-next-line sonarjs/no-invariant-returns
function onSubmit(e) { function onSubmit(e) {
const file = currentFile; 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 lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../scripts/settings/userSettings';
import './style.scss'; import './style.scss';
// eslint-disable-next-line compat/compat
const worker = new Worker(); const worker = new Worker();
const targetDic = {}; const targetDic = {};
worker.addEventListener( worker.addEventListener(

View file

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

View file

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

View file

@ -60,7 +60,7 @@ function getProgramInfoHtml(item, options) {
miscInfo.push(text); miscInfo.push(text);
} catch (e) { } 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); text = datetime.toLocaleDateString(date);
miscInfo.push(text); miscInfo.push(text);
} catch (e) { } 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); miscInfo.push(text);
} }
} catch (e) { } 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}`; text += ` - ${endYear}`;
} }
} catch (e) { } 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)); text = globalize.translate('OriginalAirDateValue', datetime.toLocaleDateString(date));
miscInfo.push(text); miscInfo.push(text);
} catch (e) { } catch (e) {
console.error('error parsing date:', program.PremiereDate); console.error('error parsing date:', program.PremiereDate, e);
} }
} else if (program.ProductionYear && options.year !== false ) { } else if (program.ProductionYear && options.year !== false ) {
miscInfo.push(program.ProductionYear); miscInfo.push(program.ProductionYear);
@ -255,7 +255,7 @@ export function getMediaInfoHtml(item, options = {}) {
text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false }); text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
miscInfo.push(text); miscInfo.push(text);
} catch (e) { } 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); date = datetime.parseISO8601Date(item.DateCreated, true);
context.querySelector('#txtDateAdded').value = date.toISOString().slice(0, 10); context.querySelector('#txtDateAdded').value = date.toISOString().slice(0, 10);
} catch (e) { } catch {
context.querySelector('#txtDateAdded').value = ''; context.querySelector('#txtDateAdded').value = '';
} }
} else { } else {
@ -833,7 +833,7 @@ function fillItemInfo(context, item, parentalRatingOptions) {
date = datetime.parseISO8601Date(item.PremiereDate, true); date = datetime.parseISO8601Date(item.PremiereDate, true);
context.querySelector('#txtPremiereDate').value = date.toISOString().slice(0, 10); context.querySelector('#txtPremiereDate').value = date.toISOString().slice(0, 10);
} catch (e) { } catch {
context.querySelector('#txtPremiereDate').value = ''; context.querySelector('#txtPremiereDate').value = '';
} }
} else { } else {
@ -845,7 +845,7 @@ function fillItemInfo(context, item, parentalRatingOptions) {
date = datetime.parseISO8601Date(item.EndDate, true); date = datetime.parseISO8601Date(item.EndDate, true);
context.querySelector('#txtEndDate').value = date.toISOString().slice(0, 10); context.querySelector('#txtEndDate').value = date.toISOString().slice(0, 10);
} catch (e) { } catch {
context.querySelector('#txtEndDate').value = ''; context.querySelector('#txtEndDate').value = '';
} }
} else { } else {

View file

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

View file

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

View file

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

View file

@ -713,7 +713,7 @@ export default function (view) {
}, state); }, state);
} }
} catch (e) { } 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 'GamepadDPadLeft':
case 'GamepadLeftThumbstickLeft': case 'GamepadLeftThumbstickLeft':
// Ignores gamepad events that are always triggered, even when not focused. // 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); playbackManager.rewind(currentPlayer);
showOsd(btnRewind); showOsd(btnRewind);
} }
@ -1361,7 +1361,7 @@ export default function (view) {
case 'GamepadDPadRight': case 'GamepadDPadRight':
case 'GamepadLeftThumbstickRight': case 'GamepadLeftThumbstickRight':
// Ignores gamepad events that are always triggered, even when not focused. // 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); playbackManager.fastForward(currentPlayer);
showOsd(btnFastForward); showOsd(btnFastForward);
} }
@ -1712,7 +1712,7 @@ export default function (view) {
if (browser.firefox || browser.edge) { if (browser.firefox || browser.edge) {
dom.addEventListener(document, 'click', onClickCapture, { capture: true }); dom.addEventListener(document, 'click', onClickCapture, { capture: true });
} }
} catch (e) { } catch {
setBackdropTransparency(TRANSPARENCY_LEVEL.None); // reset state set in viewbeforeshow setBackdropTransparency(TRANSPARENCY_LEVEL.None); // reset state set in viewbeforeshow
appRouter.goHome(); appRouter.goHome();
} }

View file

@ -61,7 +61,7 @@ function renderUpcoming(elem, items) {
day: 'numeric' day: 'numeric'
}); });
} catch (err) { } 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'; import { useApi } from './useApi';
interface UserSettings { interface UserSettings {
customCss?: string
disableCustomCss: boolean
theme?: string theme?: string
dashboardTheme?: string dashboardTheme?: string
dateTimeLocale?: string dateTimeLocale?: string
@ -15,6 +17,9 @@ interface UserSettings {
// NOTE: This is an incomplete list of only the settings that are currently being used // NOTE: This is an incomplete list of only the settings that are currently being used
const UserSettingField = { const UserSettingField = {
// Custom CSS
CustomCss: 'customCss',
DisableCustomCss: 'disableCustomCss',
// Theme settings // Theme settings
Theme: 'appTheme', Theme: 'appTheme',
DashboardTheme: 'dashboardTheme', DashboardTheme: 'dashboardTheme',
@ -23,11 +28,15 @@ const UserSettingField = {
Language: 'language' Language: 'language'
}; };
const UserSettingsContext = createContext<UserSettings>({}); const UserSettingsContext = createContext<UserSettings>({
disableCustomCss: false
});
export const useUserSettings = () => useContext(UserSettingsContext); export const useUserSettings = () => useContext(UserSettingsContext);
export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children }) => { export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const [ customCss, setCustomCss ] = useState<string>();
const [ disableCustomCss, setDisableCustomCss ] = useState(false);
const [ theme, setTheme ] = useState<string>(); const [ theme, setTheme ] = useState<string>();
const [ dashboardTheme, setDashboardTheme ] = useState<string>(); const [ dashboardTheme, setDashboardTheme ] = useState<string>();
const [ dateTimeLocale, setDateTimeLocale ] = useState<string>(); const [ dateTimeLocale, setDateTimeLocale ] = useState<string>();
@ -36,14 +45,25 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children
const { user } = useApi(); const { user } = useApi();
const context = useMemo<UserSettings>(() => ({ const context = useMemo<UserSettings>(() => ({
customCss,
disableCustomCss,
theme, theme,
dashboardTheme, dashboardTheme,
dateTimeLocale, dateTimeLocale,
locale: language locale: language
}), [ theme, dashboardTheme, dateTimeLocale, language ]); }), [
customCss,
disableCustomCss,
theme,
dashboardTheme,
dateTimeLocale,
language
]);
// Update the values of the user settings // Update the values of the user settings
const updateUserSettings = useCallback(() => { const updateUserSettings = useCallback(() => {
setCustomCss(userSettings.customCss());
setDisableCustomCss(userSettings.disableCustomCss());
setTheme(userSettings.theme()); setTheme(userSettings.theme());
setDashboardTheme(userSettings.dashboardTheme()); setDashboardTheme(userSettings.dashboardTheme());
setDateTimeLocale(userSettings.dateTimeLocale()); setDateTimeLocale(userSettings.dateTimeLocale());

View file

@ -17,7 +17,6 @@ import { loadCoreDictionary } from 'lib/globalize/loader';
import { initialize as initializeAutoCast } from 'scripts/autocast'; import { initialize as initializeAutoCast } from 'scripts/autocast';
import browser from './scripts/browser'; import browser from './scripts/browser';
import keyboardNavigation from './scripts/keyboardNavigation'; import keyboardNavigation from './scripts/keyboardNavigation';
import { currentSettings } from './scripts/settings/userSettings';
import { getPlugins } from './scripts/settings/webSettings'; import { getPlugins } from './scripts/settings/webSettings';
import taskButton from './scripts/taskbutton'; import taskButton from './scripts/taskbutton';
import { pageClassOn, serverAddress } from './utils/dashboard'; import { pageClassOn, serverAddress } from './utils/dashboard';
@ -116,9 +115,6 @@ build: ${__JF_BUILD_VERSION__}`);
// Load platform specific features // Load platform specific features
loadPlatformFeatures(); loadPlatformFeatures();
// Load custom CSS styles
loadCustomCss();
// Enable navigation controls // Enable navigation controls
keyboardNavigation.enable(); keyboardNavigation.enable();
autoFocuser.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() { function registerServiceWorker() {
/* eslint-disable compat/compat */
if (navigator.serviceWorker && window.appMode !== 'cordova' && window.appMode !== 'android') { if (navigator.serviceWorker && window.appMode !== 'cordova' && window.appMode !== 'android') {
navigator.serviceWorker.register('serviceworker.js').then(() => navigator.serviceWorker.register('serviceworker.js').then(() =>
console.log('serviceWorker registered') console.log('serviceWorker registered')
@ -248,7 +197,6 @@ function registerServiceWorker() {
} else { } else {
console.warn('serviceWorker unsupported'); console.warn('serviceWorker unsupported');
} }
/* eslint-enable compat/compat */
} }
async function renderApp() { async function renderApp() {

View file

@ -79,7 +79,7 @@ export function updateCurrentCulture() {
let culture; let culture;
try { try {
culture = userSettings.language(); culture = userSettings.language();
} catch (err) { } catch {
console.error('no language set in user settings'); console.error('no language set in user settings');
} }
culture = culture || getDefaultLanguage(); culture = culture || getDefaultLanguage();
@ -92,7 +92,7 @@ export function updateCurrentCulture() {
let dateTimeCulture; let dateTimeCulture;
try { try {
dateTimeCulture = userSettings.dateTimeLocale(); dateTimeCulture = userSettings.dateTimeLocale();
} catch (err) { } catch {
console.error('no date format set in user settings'); console.error('no date format set in user settings');
} }

View file

@ -22,7 +22,7 @@
// text/html parsing is natively supported // text/html parsing is natively supported
return; return;
} }
} catch (ex) { /* noop */ } } catch { /* noop */ }
DOMParserPrototype.parseFromString = function (markup, type) { DOMParserPrototype.parseFromString = function (markup, type) {
if (/^\s*text\/html\s*(?:;|$)/i.test(type)) { if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {

View file

@ -8,7 +8,7 @@
try { try {
new window.KeyboardEvent('event', { bubbles: true, cancelable: true }); new window.KeyboardEvent('event', { bubbles: true, cancelable: true });
} catch (e) { } catch {
// We can't use `KeyboardEvent` in old WebKit because `initKeyboardEvent` // We can't use `KeyboardEvent` in old WebKit because `initKeyboardEvent`
// doesn't seem to populate some properties (`keyCode`, `which`) that // doesn't seem to populate some properties (`keyCode`, `which`) that
// are read-only. // are read-only.

View file

@ -13,7 +13,7 @@
if (window.Headers) { if (window.Headers) {
try { try {
new window.Headers(undefined); new window.Headers(undefined);
} catch (_) { } catch {
console.debug('patch \'Headers\' to accept \'undefined\''); console.debug('patch \'Headers\' to accept \'undefined\'');
const _Headers = window.Headers; const _Headers = window.Headers;

View file

@ -1109,7 +1109,8 @@ class ChromecastPlayer {
return this.getPlayerStateInternal()?.NowPlayingItem?.IndexNumber; 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 // not supported yet
} }
} }

View file

@ -1512,7 +1512,7 @@ export class HtmlVideoPlayer {
trackElement.removeCue(trackElement.cues[0]); trackElement.removeCue(trackElement.cues[0]);
} }
} catch (e) { } catch (e) {
console.error('error removing cue from textTrack'); console.error('error removing cue from textTrack', e);
} }
trackElement.mode = 'disabled'; 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 // 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). // (the server tollerates a range of values when client reports that is ready).
const rangeWidth = 100; // In milliseconds. const rangeWidth = 100; // In milliseconds.
// eslint-disable-next-line sonarjs/pseudo-random
const randomOffsetTicks = Math.round((Math.random() - 0.5) * rangeWidth) * Helper.TicksPerMillisecond; const randomOffsetTicks = Math.round((Math.random() - 0.5) * rangeWidth) * Helper.TicksPerMillisecond;
this.scheduleSeek(command.When, command.PositionTicks + randomOffsetTicks); this.scheduleSeek(command.When, command.PositionTicks + randomOffsetTicks);
console.debug('SyncPlay applyCommand: adding random offset to force seek:', randomOffsetTicks, command); 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. * Sets the playback rate, if supported.
* @param {number} value The playback rate. * @param {number} value The playback rate.
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
setPlaybackRate(value) { setPlaybackRate(value) {
// Do nothing. // Do nothing.
} }
@ -197,7 +197,7 @@ class GenericPlayer {
* Seeks the player to the specified position. * Seeks the player to the specified position.
* @param {number} positionTicks The new 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) { localSeek(positionTicks) {
// Override // Override
} }
@ -213,7 +213,7 @@ class GenericPlayer {
* Sends a command to the player. * Sends a command to the player.
* @param {Object} command The command. * @param {Object} command The command.
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
localSendCommand(command) { localSendCommand(command) {
// Override // Override
} }
@ -222,7 +222,7 @@ class GenericPlayer {
* Starts playback. * Starts playback.
* @param {Object} options Playback data. * @param {Object} options Playback data.
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
localPlay(options) { localPlay(options) {
// Override // Override
} }
@ -231,7 +231,7 @@ class GenericPlayer {
* Sets playing item from playlist. * Sets playing item from playlist.
* @param {string} playlistItemId The item to play. * @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) { localSetCurrentPlaylistItem(playlistItemId) {
// Override // Override
} }
@ -240,7 +240,7 @@ class GenericPlayer {
* Removes items from playlist. * Removes items from playlist.
* @param {Array} playlistItemIds The items to remove. * @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) { localRemoveFromPlaylist(playlistItemIds) {
// Override // Override
} }
@ -250,7 +250,7 @@ class GenericPlayer {
* @param {string} playlistItemId The item to move. * @param {string} playlistItemId The item to move.
* @param {number} newIndex The new position. * @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) { localMovePlaylistItem(playlistItemId, newIndex) {
// Override // Override
} }
@ -259,7 +259,7 @@ class GenericPlayer {
* Queues in the playlist. * Queues in the playlist.
* @param {Object} options Queue data. * @param {Object} options Queue data.
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
localQueue(options) { localQueue(options) {
// Override // Override
} }
@ -268,7 +268,7 @@ class GenericPlayer {
* Queues after the playing item in the playlist. * Queues after the playing item in the playlist.
* @param {Object} options Queue data. * @param {Object} options Queue data.
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
localQueueNext(options) { localQueueNext(options) {
// Override // Override
} }
@ -291,7 +291,7 @@ class GenericPlayer {
* Sets repeat mode. * Sets repeat mode.
* @param {string} value The 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) { localSetRepeatMode(value) {
// Override // Override
} }
@ -300,7 +300,7 @@ class GenericPlayer {
* Sets shuffle mode. * Sets shuffle mode.
* @param {string} value The 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) { localSetQueueShuffleMode(value) {
// Override // Override
} }

View file

@ -913,6 +913,19 @@ export default function (options) {
profile.ContainerProfiles = []; 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 = []; profile.CodecProfiles = [];
const supportsSecondaryAudio = canPlaySecondaryAudio(videoTestElement); const supportsSecondaryAudio = canPlaySecondaryAudio(videoTestElement);

View file

@ -211,7 +211,7 @@ export function getDisplayDateTime(date) {
if (typeof date === 'string') { if (typeof date === 'string') {
try { try {
date = parseISO8601Date(date, true); date = parseISO8601Date(date, true);
} catch (err) { } catch {
return date; return date;
} }
} }
@ -227,7 +227,7 @@ export function getDisplayTime(date) {
if (typeof date === 'string') { if (typeof date === 'string') {
try { try {
date = parseISO8601Date(date, true); date = parseISO8601Date(date, true);
} catch (err) { } catch {
return date; return date;
} }
} }

View file

@ -178,7 +178,7 @@ function resetThrottle(key) {
const isElectron = navigator.userAgent.toLowerCase().indexOf('electron') !== -1; const isElectron = navigator.userAgent.toLowerCase().indexOf('electron') !== -1;
function allowInput() { function allowInput() {
// This would be nice but always seems to return true with electron // 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; return false;
} }
@ -356,7 +356,6 @@ function isGamepadConnected() {
} }
function onFocusOrGamepadAttach() { function onFocusOrGamepadAttach() {
/* eslint-disable-next-line compat/compat */
if (isGamepadConnected() && document.hasFocus()) { if (isGamepadConnected() && document.hasFocus()) {
console.log('Gamepad connected! Starting input loop'); console.log('Gamepad connected! Starting input loop');
startInputLoop(); startInputLoop();
@ -364,7 +363,6 @@ function onFocusOrGamepadAttach() {
} }
function onFocusOrGamepadDetach() { function onFocusOrGamepadDetach() {
/* eslint-disable-next-line compat/compat */
if (!isGamepadConnected() || !document.hasFocus()) { if (!isGamepadConnected() || !document.hasFocus()) {
console.log('Gamepad disconnected! No other gamepads are connected, stopping input loop'); console.log('Gamepad disconnected! No other gamepads are connected, stopping input loop');
stopInputLoop(); stopInputLoop();

View file

@ -64,7 +64,7 @@ let hasFieldKey = false;
try { try {
hasFieldKey = 'key' in new KeyboardEvent('keydown'); hasFieldKey = 'key' in new KeyboardEvent('keydown');
} catch (e) { } catch (e) {
console.error("error checking 'key' field"); console.error("error checking 'key' field", e);
} }
if (!hasFieldKey) { 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 // 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); window.addEventListener('gamepadconnected', attachGamepadScript);
} }

View file

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

View file

@ -1,16 +1,7 @@
import { getDefaultTheme, getThemes as getConfiguredThemes } from './settings/webSettings'; import { getDefaultTheme, getThemes as getConfiguredThemes } from './settings/webSettings';
let themeStyleElement = document.querySelector('#cssTheme');
let currentThemeId; let currentThemeId;
function unloadTheme() {
const elem = themeStyleElement;
if (elem) {
elem.removeAttribute('href');
currentThemeId = null;
}
}
function getThemes() { function getThemes() {
return getConfiguredThemes(); return getConfiguredThemes();
} }
@ -29,11 +20,7 @@ function getThemeStylesheetInfo(id) {
theme = getDefaultTheme(); theme = getDefaultTheme();
} }
return { return theme;
stylesheetPath: 'themes/' + theme.id + '/theme.css',
themeId: theme.id,
color: theme.color
};
}); });
} }
@ -45,36 +32,12 @@ function setTheme(id) {
} }
getThemeStylesheetInfo(id).then(function (info) { getThemeStylesheetInfo(id).then(function (info) {
if (currentThemeId && currentThemeId === info.themeId) { if (currentThemeId && currentThemeId === info.id) {
resolve(); resolve();
return; return;
} }
const linkUrl = info.stylesheetPath; currentThemeId = info.id;
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;
document.getElementById('themeColor').content = info.color; document.getElementById('themeColor').content = info.color;
}); });
@ -82,6 +45,6 @@ function setTheme(id) {
} }
export default { export default {
getThemes: getThemes, getThemes,
setTheme: setTheme setTheme
}; };

View file

@ -729,7 +729,7 @@
"XmlTvSportsCategoriesHelp": "البرامج من هذه التصنيفات ستعرض كبرامج رياضية. إفصل الإدخالات المتعددة برمز \"|\".", "XmlTvSportsCategoriesHelp": "البرامج من هذه التصنيفات ستعرض كبرامج رياضية. إفصل الإدخالات المتعددة برمز \"|\".",
"Yesterday": "البارحة", "Yesterday": "البارحة",
"ConfirmDeleteImage": "حذف الصورة؟", "ConfirmDeleteImage": "حذف الصورة؟",
"ConfigureDateAdded": "قم بإعداد كيفية تحديد البيانات الوصفية ل \"تاريخ الإضافة\" في لوحة المعلومات> المكتبات> إعدادات NFO", "ConfigureDateAdded": "قم بإعداد كيفية تحديد البيانات الوصفية ل \"تاريخ الإضافة\" في لوحة المعلومات> المكتبات> العرض",
"Composer": "ألحان", "Composer": "ألحان",
"CommunityRating": "تقييم الجمهور", "CommunityRating": "تقييم الجمهور",
"ColorTransfer": "نقل اللون", "ColorTransfer": "نقل اللون",
@ -886,7 +886,7 @@
"ButtonTogglePlaylist": "قائمة التشغيل", "ButtonTogglePlaylist": "قائمة التشغيل",
"BoxSet": "طقم", "BoxSet": "طقم",
"ButtonSplit": "تقسيم", "ButtonSplit": "تقسيم",
"AllowFfmpegThrottlingHelp": "عند تفعيلها؛ فسوف تتوقف عملية الترميز transcoding توقفا مؤقتا كلما تقدمت العملية عن موضع التشغيل بنسبة كافية، تهدف هذه الخاصية إلى التقليل من استهلاك الطاقة، وتكون ذات منفعة كبيرة عندما تتم عملية المشاهدة بانتظام دون القفز عدة دقائق في المشاهدة ما بين الحينة والأخرى. كما ينطبق الأمر ذاته على عملية نسخ الملف إلى حاوية أخرى لتتوافق مع الجهاز remuxing.", "AllowFfmpegThrottlingHelp": "عند تقدم اي تحويل كود او remux لمسافة مناسبة امام نقطة اعادة التشغيل الحالية، اوقف العملية حتى يتم استهلاك موارد أقل. هذا سوف يكون مفيد عندما يتم المشاهدة بدون التقدم بشكل مستمر. قم بإطفاء الخاصية هذه عندما تواجه مشاكل في إعادة التشغيل.",
"InstallingPackage": "تثبيت {0} (الإصدار {1})", "InstallingPackage": "تثبيت {0} (الإصدار {1})",
"Images": "الصور", "Images": "الصور",
"Identify": "التعرف على الوسائط", "Identify": "التعرف على الوسائط",
@ -1052,7 +1052,7 @@
"DashboardArchitecture": "المعمارية: {0}", "DashboardArchitecture": "المعمارية: {0}",
"DailyAt": "يومياً على {0}", "DailyAt": "يومياً على {0}",
"ClearQueue": "مسح القائمة المؤقتة", "ClearQueue": "مسح القائمة المؤقتة",
"Bwdif": "BWDIF", "Bwdif": "فلتر بوب ويفر لإزالة التداخل (BWDIF)",
"ButtonPlayer": "المشغل", "ButtonPlayer": "المشغل",
"ButtonCast": "إرسال وسائط إلى جهاز", "ButtonCast": "إرسال وسائط إلى جهاز",
"HeaderSyncPlayTimeSyncSettings": "تزامن الوقت", "HeaderSyncPlayTimeSyncSettings": "تزامن الوقت",
@ -1722,7 +1722,7 @@
"LabelEnableAudioVbrHelp": "معدل البِت المتغير ينتج على جودة أفضل مقارنة بمعدل البت المتوسط، ولكن في بعض الحالات النادرة قد يسبب مشاكل في التخزين المؤقت والتوافق.", "LabelEnableAudioVbrHelp": "معدل البِت المتغير ينتج على جودة أفضل مقارنة بمعدل البت المتوسط، ولكن في بعض الحالات النادرة قد يسبب مشاكل في التخزين المؤقت والتوافق.",
"LabelSegmentKeepSecondsHelp": "الزمن بالثواني الذي يجب الاحتفاظ به للشرائح بعد أن يتم تحميلها من قبل العميل. يعمل هذا ألأعداد فقط إذا كان حذف الشرائح مفعلًا.", "LabelSegmentKeepSecondsHelp": "الزمن بالثواني الذي يجب الاحتفاظ به للشرائح بعد أن يتم تحميلها من قبل العميل. يعمل هذا ألأعداد فقط إذا كان حذف الشرائح مفعلًا.",
"AiTranslated": "مترجمة من قبل ذكاء اسطناعي", "AiTranslated": "مترجمة من قبل ذكاء اسطناعي",
"SelectAudioNormalizationHelp": "كسب الالبوم-تعديل الصوت لكل مسار لكي يعملون بنفس مستوى- كسب الالبوم- تعديل مستوى الصوت لكل المسارات في البوم واحد مع ابقاء على النطاق الديناميكي للألبوم.", "SelectAudioNormalizationHelp": "كسب الالبوم-تعديل الصوت لكل مسار لكي يعملون بنفس المستوى- كسب الالبوم- تعديل مستوى الصوت لكل المسارات في البوم واحد مع ابقاء على النطاق الديناميكي للألبوم. التحويل بين (إيقاف) والخيارات الاخرى يتطلب إعادة تشغيل playback الحالي.",
"ButtonEditUser": "تعديل مستخدم", "ButtonEditUser": "تعديل مستخدم",
"AllowSubtitleManagement": "اسمح لهذا المستخدم تعديل الترجمات", "AllowSubtitleManagement": "اسمح لهذا المستخدم تعديل الترجمات",
"HeaderDeleteSeries": "حذف مسلسل", "HeaderDeleteSeries": "حذف مسلسل",
@ -1826,5 +1826,28 @@
"LabelDisableVbrAudioEncoding": "تعطيل VBR لترميز الصوت", "LabelDisableVbrAudioEncoding": "تعطيل VBR لترميز الصوت",
"HeaderNextVideo": "الفيديو التالي", "HeaderNextVideo": "الفيديو التالي",
"LabelDevice": "الجهاز", "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": "Пастаўшчыкі сегментаў медыяфайлаў", "LabelMediaSegmentProviders": "Пастаўшчыкі сегментаў медыяфайлаў",
"MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.", "MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.",
"MediaSegmentType.Commercial": "Рэклама", "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", "MetadataNfoLoadError": "Načtení nastavení metadat v souborech NFO se nezdařilo",
"HeaderPageNotFound": "Stránka nebyla nalezena", "HeaderPageNotFound": "Stránka nebyla nalezena",
"PageNotFound": "Toto není stránka, kterou hledáš.", "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", "LabelMediaSegmentProviders": "Mediesegment tilbydere",
"MediaSegmentProvidersHelp": "Aktiver og arranger dine foretrukne mediesegment-tilbydere efter prioritering.", "MediaSegmentProvidersHelp": "Aktiver og arranger dine foretrukne mediesegment-tilbydere efter prioritering.",
"DeleteServerConfirmation": "Er du sikker på at du ønsker slette denne server?", "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", "Custom": "Brugerdefineret",
"CustomSubtitleStylingHelp": "Undertekse styling vil virke på de fleste enheder, men kommer med en præstations pris.", "CustomSubtitleStylingHelp": "Undertekse styling vil virke på de fleste enheder, men kommer med en præstations pris.",
"LabelSubtitleStyling": "Undertekst Styling", "LabelSubtitleStyling": "Undertekst Styling",

View file

@ -1885,7 +1885,7 @@
"Lyric": "Στίχος", "Lyric": "Στίχος",
"LogoScreensaver": "Λογότυπο Προφύλαξης Οθόνης", "LogoScreensaver": "Λογότυπο Προφύλαξης Οθόνης",
"MediaSegmentProvidersHelp": "Ενεργοποίηση και ταξινόμηση των προτιμωμένων παροχέων τμημάτων πολυμέσων σε σειρά προτεραιότητας.", "MediaSegmentProvidersHelp": "Ενεργοποίηση και ταξινόμηση των προτιμωμένων παροχέων τμημάτων πολυμέσων σε σειρά προτεραιότητας.",
"LibraryInvalidItemIdError": "H βιβλιοθήκη βρίσκεται σε μη έγκυρη κατάσταση και δεν μπορεί να τροποποιηθεί. Πιθανότατα αντιμετοπίζετε σφάλμα: η διαδρομή στην βάση δεδομένη δεν είναι η σωστή στον δίσκο.", "LibraryInvalidItemIdError": "H βιβλιοθήκη βρίσκεται σε μη έγκυρη κατάσταση και δεν μπορεί να τροποποιηθεί. Πιθανότατα αντιμετωπίζετε σφάλμα: η διαδρομή στην βάση δεδομένη δεν είναι η σωστή στο σύστημα αρχείων.",
"Lyrics": "Στίχοι", "Lyrics": "Στίχοι",
"MediaInfoRotation": "Προσανατολισμός", "MediaInfoRotation": "Προσανατολισμός",
"LimitSupportedVideoResolution": "Περιορισμός μέγιστης υποστηριζόμενης ανάλυσης βίντεο", "LimitSupportedVideoResolution": "Περιορισμός μέγιστης υποστηριζόμενης ανάλυσης βίντεο",
@ -1963,5 +1963,6 @@
"PlaybackError.MEDIA_NOT_SUPPORTED": "Η αναπαραγωγή απέτυχε διότι το μέσο δεν υποστηρίζεται από αυτό το πρόγραμμα.", "PlaybackError.MEDIA_NOT_SUPPORTED": "Η αναπαραγωγή απέτυχε διότι το μέσο δεν υποστηρίζεται από αυτό το πρόγραμμα.",
"PlaybackError.MEDIA_DECODE_ERROR": "Η αναπαραγωγή απέτυχε λόγω σφάλματος στην αποκωδικοποίηση του μέσου.", "PlaybackError.MEDIA_DECODE_ERROR": "Η αναπαραγωγή απέτυχε λόγω σφάλματος στην αποκωδικοποίηση του μέσου.",
"MetadataImagesLoadError": "Αποτυχία φόρτωσης ρυθμίσεων των μεταδεδομένων", "MetadataImagesLoadError": "Αποτυχία φόρτωσης ρυθμίσεων των μεταδεδομένων",
"PlaybackError.FATAL_HLS_ERROR": "Παρουσιάστηκε κρίσιμο σφάλμα στην ροή HLS." "PlaybackError.FATAL_HLS_ERROR": "Παρουσιάστηκε κρίσιμο σφάλμα στην ροή HLS.",
"SettingsPageLoadError": "Αποτυχία φόρτωσης σελίδας ρυθμίσεων"
} }

View file

@ -1719,7 +1719,7 @@
"Short": "Short", "Short": "Short",
"HeaderPerformance": "Performance", "HeaderPerformance": "Performance",
"LabelParallelImageEncodingLimit": "Parallel image encoding limit", "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", "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.", "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", "LabelTonemappingMode": "Tone mapping mode",
@ -1880,7 +1880,7 @@
"LabelSelectPreferredTranscodeVideoAudioCodec": "Preferred transcode audio codec in video playback", "LabelSelectPreferredTranscodeVideoAudioCodec": "Preferred transcode audio codec in video playback",
"Letterer": "Letterer", "Letterer": "Letterer",
"LibraryScanFanoutConcurrency": "Parallel library scan tasks limit", "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", "Penciller": "Penciler",
"PlaylistError.AddFailed": "Error adding to playlist", "PlaylistError.AddFailed": "Error adding to playlist",
"PlaylistError.CreateFailed": "Error creating playlist", "PlaylistError.CreateFailed": "Error creating playlist",
@ -2011,5 +2011,7 @@
"DisplayLoadError": "An error occurred while loading display configuration data.", "DisplayLoadError": "An error occurred while loading display configuration data.",
"HeaderPageNotFound": "Page not found", "HeaderPageNotFound": "Page not found",
"PageNotFound": "This is not the page you are looking for.", "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", "ReplaceExistingImages": "Replace existing images",
"ReplaceTrickplayImages": "Replace existing trickplay images", "ReplaceTrickplayImages": "Replace existing trickplay images",
"Retry": "Retry", "Retry": "Retry",
"RetryWithGlobalSearch": "Retry with a global search",
"Reset": "Reset", "Reset": "Reset",
"ResetPassword": "Reset Password", "ResetPassword": "Reset Password",
"ResolutionMatchSource": "Match Source", "ResolutionMatchSource": "Match Source",
@ -1530,6 +1531,7 @@
"StoryArc": "Story Arc", "StoryArc": "Story Arc",
"StopPlayback": "Stop playback", "StopPlayback": "Stop playback",
"StopRecording": "Stop recording", "StopRecording": "Stop recording",
"StreamCountExceedsLimit": "The number of streams exceeds the limit",
"Studio": "Studio", "Studio": "Studio",
"Studios": "Studios", "Studios": "Studios",
"Subtitle": "Subtitle", "Subtitle": "Subtitle",

View file

@ -1664,8 +1664,8 @@
"MediaInfoVideoRangeType": "Videon aluetyyppi", "MediaInfoVideoRangeType": "Videon aluetyyppi",
"LabelVideoRangeType": "Videon aluetyyppi", "LabelVideoRangeType": "Videon aluetyyppi",
"VideoRangeTypeNotSupported": "Videon aluetyyppiä ei tueta", "VideoRangeTypeNotSupported": "Videon aluetyyppiä ei tueta",
"LabelVppTonemappingContrastHelp": "Käytä kontrastin vahvistusta VPP-sävykartoituksen kanssa. Suositus- ja oletusarvo on 1.", "LabelVppTonemappingContrastHelp": "Käytä kontrastin vahvistusta VPP-sävykartoituksen kanssa. Suositusarvo on 1.",
"LabelVppTonemappingBrightnessHelp": "Käytä kirkkauden vahvistusta VPP-sävykartoituksen kanssa. Suositus- ja oletusarvot ovat 10 ja 0.", "LabelVppTonemappingBrightnessHelp": "Käytä kirkkauden vahvistusta VPP-sävykartoituksen kanssa. Suositusarvo on 16.",
"LabelVppTonemappingContrast": "VPP-sävykartoituksen kontrastin vahvistus", "LabelVppTonemappingContrast": "VPP-sävykartoituksen kontrastin vahvistus",
"LabelVppTonemappingBrightness": "VPP-sävykartoituksen kirkkauden vahvistus", "LabelVppTonemappingBrightness": "VPP-sävykartoituksen kirkkauden vahvistus",
"IgnoreDtsHelp": "Valinnan poistaminen voi korjata joitakin ongelmia, kuten puuttuvan äänen kanavilla joilla on erilliset ääni- ja videovirrat.", "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.Critical": "Kriittinen",
"LogLevel.None": "Ei mitään", "LogLevel.None": "Ei mitään",
"HeaderEpisodesStatus": "Jaksojen tila", "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", "AllowSegmentDeletion": "Poista osiot",
"LabelThrottleDelaySeconds": "Rajoita kun on kulunut", "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ä.", "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.", "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", "Editor": "Ohjaus",
"Letterer": "Kirjoittaja", "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.", "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.", "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", "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", "HeaderVideoAdvanced": "Edistynyt video",
"PlaylistPublicDescription": "Salli tämän soittolistan katsominen jokaiselle kirjautuneelle käyttäjälle.", "PlaylistPublicDescription": "Salli tämän soittolistan katsominen jokaiselle kirjautuneelle käyttäjälle.",
"DateModified": "Muokkauspäivämäärä", "DateModified": "Muokkauspäivämäärä",
@ -1985,5 +1985,12 @@
"Retry": "Yritä uudelleen", "Retry": "Yritä uudelleen",
"Reset": "Nollaa", "Reset": "Nollaa",
"ReplaceTrickplayImages": "Korvaa nykyiset trickplay kuvat", "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", "LabelVideo": "Vidéo",
"DashboardArchitecture": "Architecture : {0}", "DashboardArchitecture": "Architecture : {0}",
"DashboardOperatingSystem": "Système d'exploitation: {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)", "Composer": "Compositeur(trice)",
"CommunityRating": "Évaluation de la communauté", "CommunityRating": "Évaluation de la communauté",
"ColorTransfer": "Transfert de couleur", "ColorTransfer": "Transfert de couleur",
@ -1183,7 +1183,7 @@
"LabelNumberOfGuideDays": "Nombre de jours de guide à télécharger", "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.", "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", "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", "LabelPasswordResetProvider": "Fournisseur de récupération de mot de passe",
"LabelPlayDefaultAudioTrack": "Lire la piste audio par défaut peu importe la langue", "LabelPlayDefaultAudioTrack": "Lire la piste audio par défaut peu importe la langue",
"LabelPostProcessor": "Application de post-traitement", "LabelPostProcessor": "Application de post-traitement",
@ -1353,7 +1353,7 @@
"Letterer": "Lettreur", "Letterer": "Lettreur",
"Next": "Suivant", "Next": "Suivant",
"LibraryScanFanoutConcurrency": "Limite de tâches de scan de médiathèque en parallèle", "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", "OptionEveryday": "Tous les jours",
"OptionForceRemoteSourceTranscoding": "Forcer le transcodage pour les sources de média externes comme la télé en direct", "OptionForceRemoteSourceTranscoding": "Forcer le transcodage pour les sources de média externes comme la télé en direct",
"OptionHasThemeVideo": "Générique", "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.", "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.", "LabelTileWidthHelp": "Nombre maximum d'images par tuile dans la direction X.",
"LabelTileHeight": "Hauteur des tuiles", "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.", "LibraryNameInvalid": "Le nom de la bibliothèque ne peut pas être vide.",
"HeaderPageNotFound": "Page introuvable", "HeaderPageNotFound": "Page introuvable",
"PageNotFound": "Ceci n'est pas la page que vous cherchez.", "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", "ClearQueue": "Očisti red",
"Bwdif": "BWDIF", "Bwdif": "BWDIF",
"ButtonPlayer": "Reproduktor", "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.", "LabelCreateHttpPortMap": "Omogući automatsko mapiranje ulaza za HTTP i HTTPS promet.",
"LabelChromecastVersion": "Google Cast verzija", "LabelChromecastVersion": "Google Cast verzija",
"LabelCertificatePasswordHelp": "Ako Vaš certifikat zahtjeva lozinku, molimo unesite je ovdje.", "LabelCertificatePasswordHelp": "Ako Vaš certifikat zahtjeva lozinku, molimo unesite je ovdje.",
@ -1570,7 +1570,7 @@
"DisplayLoadError": "Dogodila se pogreška tijekom prikazivanja podataka za konfiguraciju.", "DisplayLoadError": "Dogodila se pogreška tijekom prikazivanja podataka za konfiguraciju.",
"EnableLibrary": "Uključite biblioteku", "EnableLibrary": "Uključite biblioteku",
"EnableLibraryHelp": "Isključivanje bibliotekeće ju sakriti od svih korisnika.", "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.", "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.", "AlwaysRemuxMp3AudioFilesHelp": "Ako imate datoteke za koje Vaš preglednik neprecizno izračunava vremenske oznake, uključite ovo kao zaobilazak.",
"EditLyrics": "Uredi tekst pjesme", "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.", "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?", "DeleteServerConfirmation": "Biztos, hogy törli ezt a kiszolgálót?",
"VideoCodecTagNotSupported": "A videókodek-címke nem támogatott", "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", "MetadataNfoLoadError": "Errore nel caricamento dei metadati NFO",
"HeaderPageNotFound": "Pagina non trovata", "HeaderPageNotFound": "Pagina non trovata",
"PageNotFound": "Questa non è la pagina che stai cercando.", "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", "LabelReleaseDate": "Izlaiduma datums",
"LabelPreferredSubtitleLanguage": "Ieteicamā subtitru valoda", "LabelPreferredSubtitleLanguage": "Ieteicamā subtitru valoda",
"LabelPlayerDimensions": "Atskaņotāja dimensijas", "LabelPlayerDimensions": "Atskaņotāja dimensijas",
"LabelParentalRating": "Vecāku reitings", "LabelParentalRating": "Vecuma reitings",
"LabelMonitorUsers": "Uzraudzīt aktivitāti no", "LabelMonitorUsers": "Uzraudzīt aktivitāti no",
"LabelMinResumePercentageHelp": "Vienumi tiek uzskatīti par neatskaņotiem, ja apturēti pirms šī laika.", "LabelMinResumePercentageHelp": "Vienumi tiek uzskatīti par neatskaņotiem, ja apturēti pirms šī laika.",
"LabelMinResumePercentage": "Minimālais turpināšanas procents", "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.", "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.", "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", "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", "HeaderDummyChapter": "Nodaļu attēli",
"EnableCardLayout": "Padarīt redzamu CardBox", "EnableCardLayout": "Padarīt redzamu CardBox",
"MessageConfirmDeleteGuideProvider": "Vai tiešām vēlaties izdzēst šo ceļveža pakalpojumu sniedzēju?", "MessageConfirmDeleteGuideProvider": "Vai tiešām vēlaties izdzēst šo ceļveža pakalpojumu sniedzēju?",
@ -1748,7 +1748,7 @@
"LabelBuildVersion": "Kompilācijas versija", "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.", "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", "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", "PlaylistPublic": "Atļaut publisku piekļuvi",
"PlaylistPublicDescription": "Ļaut šo atskaņošanas sarakstu skatīt jebkuram autentificētam lietotājam.", "PlaylistPublicDescription": "Ļaut šo atskaņošanas sarakstu skatīt jebkuram autentificētam lietotājam.",
"Rate": "Vertējums", "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.", "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ā", "LabelExtractTrickplayDuringLibraryScan": "Izgūt trickplay attēlus bibliotēkas skenēšanas laikā",
"LabelJpegQualityHelp": "Trickplay attēlu JPEG kompresijas kvalitātes lielums.", "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", "HeaderBranding": "Merking",
"HeaderCancelRecording": "Avbryt opptak", "HeaderCancelRecording": "Avbryt opptak",
"HeaderCancelSeries": "Avbryt serie", "HeaderCancelSeries": "Avbryt serie",
"HeaderCastAndCrew": "Skuespillere & mannskap", "HeaderCastAndCrew": "Medvirkende",
"HeaderChannelAccess": "Kanal-tilgang", "HeaderChannelAccess": "Kanal-tilgang",
"HeaderCodecProfile": "Kodekprofil", "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.", "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.", "MessageGetInstalledPluginsError": "En feil oppstod ved henting av listen over installerte tillegg.",
"MessagePluginInstallError": "En feil oppstod ved installasjon av tillegget.", "MessagePluginInstallError": "En feil oppstod ved installasjon av tillegget.",
"ThumbCard": "Miniatyrbildekort", "ThumbCard": "Miniatyrbildekort",
"SpecialFeatures": "Spesialfunksjoner", "SpecialFeatures": "Ekstra innhold",
"PosterCard": "Plakatkort", "PosterCard": "Plakatkort",
"Video": "Video", "Video": "Video",
"Subtitle": "Undertekst", "Subtitle": "Undertekst",
@ -1720,7 +1720,7 @@
"Studio": "Studio", "Studio": "Studio",
"SubtitleCyan": "Turkis", "SubtitleCyan": "Turkis",
"UserMenu": "Brukermenyen", "UserMenu": "Brukermenyen",
"Featurette": "Novellefilm", "Featurette": "Featurette",
"LabelTonemappingMode": "Tonemappingsmodus", "LabelTonemappingMode": "Tonemappingsmodus",
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Ekstramateriale har ofte det samme innebygde navnet som det opprinnelige materialet. Kryss av for denne for å bruke den innebygde tittelen likevel.", "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", "LabelSyncPlayNoGroups": "Ingen grupper tilgjengelig",

View file

@ -2011,5 +2011,6 @@
"MetadataNfoLoadError": "Laden van metadata-NFO-instellingen mislukt", "MetadataNfoLoadError": "Laden van metadata-NFO-instellingen mislukt",
"PageNotFound": "Dit is niet de pagina die je zoekt.", "PageNotFound": "Dit is niet de pagina die je zoekt.",
"HeaderPageNotFound": "Pagina niet gevonden", "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", "MetadataNfoLoadError": "Nie udało się załadować ustawień metadanych NFO",
"HeaderPageNotFound": "Nie znaleziono strony", "HeaderPageNotFound": "Nie znaleziono strony",
"PageNotFound": "To nie jest strona, której szukasz.", "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", "Retry": "Tentar novamente",
"LogLoadFailure": "Falha ao carregar o ficheiro de registos. É possível que atualmente esteja a ser escrito.", "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", "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?", "DeleteServerConfirmation": "Tens a certeza de que queres eliminar este servidor?",
"LibraryNameInvalid": "O nome da biblioteca não pode estar vazio.", "LibraryNameInvalid": "O nome da biblioteca não pode estar vazio.",
"MetadataNfoLoadError": "Falha ao carregar as definições de metadados NFO", "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": "Не удалось загрузить настройки метаданных", "MetadataImagesLoadError": "Не удалось загрузить настройки метаданных",
"HeaderPageNotFound": "Станица не найдена", "HeaderPageNotFound": "Станица не найдена",
"PageNotFound": "Это не та страница, которую вы искали.", "PageNotFound": "Это не та страница, которую вы искали.",
"SettingsPageLoadError": "Не удалось загрузить страницу параметров" "SettingsPageLoadError": "Не удалось загрузить страницу параметров",
"RetryWithGlobalSearch": "Повторите попытку с помощью глобального поиска",
"StreamCountExceedsLimit": "Количество потоков превышает предельное значение"
} }

View file

@ -2012,5 +2012,7 @@
"MetadataNfoLoadError": "Nepodarilo sa načítať nastavenia NFO metadát", "MetadataNfoLoadError": "Nepodarilo sa načítať nastavenia NFO metadát",
"HeaderPageNotFound": "Stránka nebola nájdená", "HeaderPageNotFound": "Stránka nebola nájdená",
"PageNotFound": "Toto nie je stránka, ktorú hľadáš.", "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", "MetadataNfoLoadError": "Не вдалося завантажити налаштування метаданих NFO",
"HeaderPageNotFound": "Сторінку не знайдено", "HeaderPageNotFound": "Сторінку не знайдено",
"PageNotFound": "Це не та сторінка, яку ви шукаєте.", "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", "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.", "PageNotFound": "Đây không phải là trang bạn đang tìm kiếm.",
"HeaderPageNotFound": "Không tìm thấy trang", "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