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:
commit
bac03437f7
108 changed files with 2005 additions and 1723 deletions
|
@ -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",
|
||||||
|
|
6
.github/workflows/__codeql.yml
vendored
6
.github/workflows/__codeql.yml
vendored
|
@ -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}}'
|
||||||
|
|
|
@ -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
1418
package-lock.json
generated
File diff suppressed because it is too large
Load diff
39
package.json
39
package.json
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -148,11 +148,13 @@ const NewTriggerForm: FunctionComponent<IProps> = ({ open, title, onClose, onAdd
|
||||||
fullWidth
|
fullWidth
|
||||||
defaultValue={''}
|
defaultValue={''}
|
||||||
type='number'
|
type='number'
|
||||||
inputProps={{
|
|
||||||
min: 1,
|
|
||||||
step: 0.5
|
|
||||||
}}
|
|
||||||
label={globalize.translate('LabelTimeLimitHours')}
|
label={globalize.translate('LabelTimeLimitHours')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
step: 0.5
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -81,12 +81,14 @@ export const Component = () => {
|
||||||
name='MinResumePercentage'
|
name='MinResumePercentage'
|
||||||
type='number'
|
type='number'
|
||||||
defaultValue={config?.MinResumePct}
|
defaultValue={config?.MinResumePct}
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelMinResumePercentageHelp')}
|
helperText={globalize.translate('LabelMinResumePercentageHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -94,12 +96,14 @@ export const Component = () => {
|
||||||
name='MaxResumePercentage'
|
name='MaxResumePercentage'
|
||||||
type='number'
|
type='number'
|
||||||
defaultValue={config?.MaxResumePct}
|
defaultValue={config?.MaxResumePct}
|
||||||
inputProps={{
|
|
||||||
min: 1,
|
|
||||||
max: 100,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
|
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -107,12 +111,14 @@ export const Component = () => {
|
||||||
name='MinAudiobookResume'
|
name='MinAudiobookResume'
|
||||||
type='number'
|
type='number'
|
||||||
defaultValue={config?.MinAudiobookResume}
|
defaultValue={config?.MinAudiobookResume}
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
|
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -120,12 +126,14 @@ export const Component = () => {
|
||||||
name='MaxAudiobookResume'
|
name='MaxAudiobookResume'
|
||||||
type='number'
|
type='number'
|
||||||
defaultValue={config?.MaxAudiobookResume}
|
defaultValue={config?.MaxAudiobookResume}
|
||||||
inputProps={{
|
|
||||||
min: 1,
|
|
||||||
max: 100,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
|
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -133,11 +141,13 @@ export const Component = () => {
|
||||||
name='MinResumeDuration'
|
name='MinResumeDuration'
|
||||||
type='number'
|
type='number'
|
||||||
defaultValue={config?.MinResumeDurationSeconds}
|
defaultValue={config?.MinResumeDurationSeconds}
|
||||||
inputProps={{
|
|
||||||
min: 0,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelMinResumeDurationHelp')}
|
helperText={globalize.translate('LabelMinResumeDurationHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 0,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -158,22 +158,26 @@ export const Component = () => {
|
||||||
type='number'
|
type='number'
|
||||||
inputMode='numeric'
|
inputMode='numeric'
|
||||||
defaultValue={defaultConfig.TrickplayOptions?.Interval}
|
defaultValue={defaultConfig.TrickplayOptions?.Interval}
|
||||||
inputProps={{
|
|
||||||
min: 1,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelImageIntervalHelp')}
|
helperText={globalize.translate('LabelImageIntervalHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={globalize.translate('LabelWidthResolutions')}
|
label={globalize.translate('LabelWidthResolutions')}
|
||||||
name='WidthResolutions'
|
name='WidthResolutions'
|
||||||
defaultValue={defaultConfig.TrickplayOptions?.WidthResolutions}
|
defaultValue={defaultConfig.TrickplayOptions?.WidthResolutions}
|
||||||
inputProps={{
|
|
||||||
required: true,
|
|
||||||
pattern: '[0-9,]*'
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelWidthResolutionsHelp')}
|
helperText={globalize.translate('LabelWidthResolutionsHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
required: true,
|
||||||
|
pattern: '[0-9,]*'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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={{
|
|
||||||
min: 1,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelTileWidthHelp')}
|
helperText={globalize.translate('LabelTileWidthHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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={{
|
|
||||||
min: 1,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelTileHeightHelp')}
|
helperText={globalize.translate('LabelTileHeightHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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={{
|
|
||||||
min: 1,
|
|
||||||
max: 100,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelJpegQualityHelp')}
|
helperText={globalize.translate('LabelJpegQualityHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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={{
|
|
||||||
min: 2,
|
|
||||||
max: 31,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelQscaleHelp')}
|
helperText={globalize.translate('LabelQscaleHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 2,
|
||||||
|
max: 31,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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={{
|
|
||||||
min: 0,
|
|
||||||
required: true
|
|
||||||
}}
|
|
||||||
helperText={globalize.translate('LabelTrickplayThreadsHelp')}
|
helperText={globalize.translate('LabelTrickplayThreadsHelp')}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
min: 0,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -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,14 +187,16 @@ export const Component = () => {
|
||||||
helperText={globalize.translate('LabelCachePathHelp')}
|
helperText={globalize.translate('LabelCachePathHelp')}
|
||||||
value={cachePath}
|
value={cachePath}
|
||||||
onChange={onCachePathChange}
|
onChange={onCachePathChange}
|
||||||
InputProps={{
|
slotProps={{
|
||||||
endAdornment: (
|
input: {
|
||||||
<InputAdornment position='end'>
|
endAdornment: (
|
||||||
<IconButton edge='end' onClick={showCachePathPicker}>
|
<InputAdornment position='end'>
|
||||||
<SearchIcon />
|
<IconButton edge='end' onClick={showCachePathPicker}>
|
||||||
</IconButton>
|
<SearchIcon />
|
||||||
</InputAdornment>
|
</IconButton>
|
||||||
)
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -202,14 +206,16 @@ export const Component = () => {
|
||||||
helperText={globalize.translate('LabelMetadataPathHelp')}
|
helperText={globalize.translate('LabelMetadataPathHelp')}
|
||||||
value={metadataPath}
|
value={metadataPath}
|
||||||
onChange={onMetadataPathChange}
|
onChange={onMetadataPathChange}
|
||||||
InputProps={{
|
slotProps={{
|
||||||
endAdornment: (
|
input: {
|
||||||
<InputAdornment position='end'>
|
endAdornment: (
|
||||||
<IconButton edge='end' onClick={showMetadataPathPicker}>
|
<InputAdornment position='end'>
|
||||||
<SearchIcon />
|
<IconButton edge='end' onClick={showMetadataPathPicker}>
|
||||||
</IconButton>
|
<SearchIcon />
|
||||||
</InputAdornment>
|
</IconButton>
|
||||||
)
|
</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'>
|
||||||
|
|
|
@ -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,52 +31,56 @@ export const Component = () => {
|
||||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
|
<>
|
||||||
<StrictMode>
|
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
|
||||||
<ElevationScroll elevate={false}>
|
<StrictMode>
|
||||||
<AppBar
|
<ElevationScroll elevate={false}>
|
||||||
position='fixed'
|
<AppBar
|
||||||
sx={{
|
position='fixed'
|
||||||
width: {
|
sx={{
|
||||||
xs: '100%',
|
width: {
|
||||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
xs: '100%',
|
||||||
},
|
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||||
ml: {
|
},
|
||||||
xs: 0,
|
ml: {
|
||||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
xs: 0,
|
||||||
}
|
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<AppToolbar
|
>
|
||||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
<AppToolbar
|
||||||
isDrawerOpen={isDrawerOpen}
|
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||||
onDrawerButtonClick={onToggleDrawer}
|
isDrawerOpen={isDrawerOpen}
|
||||||
/>
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
</AppBar>
|
/>
|
||||||
</ElevationScroll>
|
</AppBar>
|
||||||
|
</ElevationScroll>
|
||||||
|
|
||||||
{
|
{
|
||||||
isDrawerAvailable && (
|
isDrawerAvailable && (
|
||||||
<AppDrawer
|
<AppDrawer
|
||||||
open={isDrawerOpen}
|
open={isDrawerOpen}
|
||||||
onClose={onToggleDrawer}
|
onClose={onToggleDrawer}
|
||||||
onOpen={onToggleDrawer}
|
onOpen={onToggleDrawer}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component='main'
|
component='main'
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AppBody>
|
</AppBody>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
<ThemeCss />
|
||||||
|
<CustomCss />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -146,18 +146,20 @@ 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={{
|
|
||||||
inputMode: 'numeric',
|
|
||||||
max: '3600',
|
|
||||||
min: '1',
|
|
||||||
pattern: '[0-9]',
|
|
||||||
required: true,
|
|
||||||
step: '1',
|
|
||||||
type: 'number'
|
|
||||||
}}
|
|
||||||
label={globalize.translate('LabelBackdropScreensaverInterval')}
|
label={globalize.translate('LabelBackdropScreensaverInterval')}
|
||||||
name='screensaverInterval'
|
name='screensaverInterval'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '3600',
|
||||||
|
min: '1',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1',
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<FormHelperText id='display-settings-screensaver-interval-description'>
|
<FormHelperText id='display-settings-screensaver-interval-description'>
|
||||||
{globalize.translate('LabelBackdropScreensaverIntervalHelp')}
|
{globalize.translate('LabelBackdropScreensaverIntervalHelp')}
|
||||||
|
|
|
@ -24,19 +24,21 @@ 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={{
|
|
||||||
type: 'number',
|
|
||||||
inputMode: 'numeric',
|
|
||||||
max: '1000',
|
|
||||||
min: '0',
|
|
||||||
pattern: '[0-9]',
|
|
||||||
required: true,
|
|
||||||
step: '1'
|
|
||||||
}}
|
|
||||||
value={values.libraryPageSize}
|
value={values.libraryPageSize}
|
||||||
label={globalize.translate('LabelLibraryPageSize')}
|
label={globalize.translate('LabelLibraryPageSize')}
|
||||||
name='libraryPageSize'
|
name='libraryPageSize'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
type: 'number',
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '1000',
|
||||||
|
min: '0',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<FormHelperText id='display-settings-lib-pagesize-description'>
|
<FormHelperText id='display-settings-lib-pagesize-description'>
|
||||||
{globalize.translate('LabelLibraryPageSizeHelp')}
|
{globalize.translate('LabelLibraryPageSizeHelp')}
|
||||||
|
|
|
@ -25,18 +25,20 @@ 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={{
|
|
||||||
type: 'number',
|
|
||||||
inputMode: 'numeric',
|
|
||||||
max: '1000',
|
|
||||||
min: '0',
|
|
||||||
pattern: '[0-9]',
|
|
||||||
required: true,
|
|
||||||
step: '1'
|
|
||||||
}}
|
|
||||||
label={globalize.translate('LabelMaxDaysForNextUp')}
|
label={globalize.translate('LabelMaxDaysForNextUp')}
|
||||||
name='maxDaysForNextUp'
|
name='maxDaysForNextUp'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
type: 'number',
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '1000',
|
||||||
|
min: '0',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<FormHelperText id='display-settings-max-days-next-up-description'>
|
<FormHelperText id='display-settings-max-days-next-up-description'>
|
||||||
{globalize.translate('LabelMaxDaysForNextUpHelp')}
|
{globalize.translate('LabelMaxDaysForNextUpHelp')}
|
||||||
|
|
|
@ -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>
|
<>
|
||||||
<Outlet />
|
<AppBody>
|
||||||
</AppBody>
|
<Outlet />
|
||||||
|
</AppBody>
|
||||||
|
<ThemeCss />
|
||||||
|
<CustomCss />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
23
src/apps/stable/features/search/api/fetchItemsByType.ts
Normal file
23
src/apps/stable/features/search/api/fetchItemsByType.ts
Normal 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;
|
||||||
|
};
|
49
src/apps/stable/features/search/api/useArtistsSearch.ts
Normal file
49
src/apps/stable/features/search/api/useArtistsSearch.ts
Normal 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))
|
||||||
|
});
|
||||||
|
};
|
150
src/apps/stable/features/search/api/useLiveTvSearch.ts
Normal file
150
src/apps/stable/features/search/api/useLiveTvSearch.ts
Normal 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)
|
||||||
|
});
|
||||||
|
};
|
50
src/apps/stable/features/search/api/usePeopleSearch.ts
Normal file
50
src/apps/stable/features/search/api/usePeopleSearch.ts
Normal 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
|
||||||
|
});
|
||||||
|
};
|
50
src/apps/stable/features/search/api/useProgramsSearch.ts
Normal file
50
src/apps/stable/features/search/api/useProgramsSearch.ts
Normal 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
|
||||||
|
});
|
||||||
|
};
|
98
src/apps/stable/features/search/api/useSearchItems.ts
Normal file
98
src/apps/stable/features/search/api/useSearchItems.ts
Normal 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
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
|
@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
57
src/apps/stable/features/search/api/useVideoSearch.ts
Normal file
57
src/apps/stable/features/search/api/useVideoSearch.ts
Normal 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
|
||||||
|
});
|
||||||
|
};
|
|
@ -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 {
|
|
@ -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>
|
||||||
);
|
);
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
};
|
12
src/apps/stable/features/search/constants/queryOptions.ts
Normal file
12
src/apps/stable/features/search/constants/queryOptions.ts
Normal 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
|
||||||
|
};
|
|
@ -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'
|
||||||
|
];
|
8
src/apps/stable/features/search/types.ts
Normal file
8
src/apps/stable/features/search/types.ts
Normal 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;
|
||||||
|
};
|
141
src/apps/stable/features/search/utils/search.ts
Normal file
141
src/apps/stable/features/search/utils/search.ts
Normal 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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
26
src/components/CustomCss.tsx
Normal file
26
src/components/CustomCss.tsx
Normal 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;
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
34
src/components/ThemeCss.tsx
Normal file
34
src/components/ThemeCss.tsx
Normal 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;
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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++) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './useSearchItems';
|
|
||||||
export * from './useSearchSuggestions';
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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());
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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": "إجراءات قطاع الوسائط"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1955,5 +1955,9 @@
|
||||||
"LabelMediaSegmentProviders": "Пастаўшчыкі сегментаў медыяфайлаў",
|
"LabelMediaSegmentProviders": "Пастаўшчыкі сегментаў медыяфайлаў",
|
||||||
"MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.",
|
"MediaSegmentProvidersHelp": "Уключыце і расстаўце вашых пераважных пастаўшчыкоў сегментаў медыяфайлаў у парадку прыярытэту.",
|
||||||
"MediaSegmentType.Commercial": "Рэклама",
|
"MediaSegmentType.Commercial": "Рэклама",
|
||||||
"MediaSegmentAction.None": "Няма"
|
"MediaSegmentAction.None": "Няма",
|
||||||
|
"HeaderNextVideo": "Наступнае відэа",
|
||||||
|
"HeaderPageNotFound": "Старонка не знойдзена",
|
||||||
|
"LabelSaveTrickplayLocally": "Захаваць выявы trickplay побач з медыяфайламі",
|
||||||
|
"LabelSaveTrickplayLocallyHelp": "Захаванне выяў trickplay у тэчках з медыяфайламі дазволіць размясціць іх побач з вашымі медыяфайламі для зручнай міграцыі і доступу."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "Αποτυχία φόρτωσης σελίδας ρυθμίσεων"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 d’application FFmpeg prenant en charge l’accé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 d’application FFmpeg prenant en charge l’accélération OpenCL est requis.",
|
||||||
"LabelParentNumber": "Numéro parent",
|
"LabelParentNumber": "Numéro parent",
|
||||||
"LabelParallelImageEncodingLimitHelp": "Nombre maximal d’encodages d’images qui peuvent être exécutés en parallèle. Une valeur de 0 entraînera une sélection automatique d’une limite selon le nombre de coeurs de votre système.",
|
"LabelParallelImageEncodingLimitHelp": "Nombre maximal d’encodages d’images qui peuvent être exécutés en parallèle. Une valeur vide entraînera une sélection automatique d’une limite selon le nombre de coeurs de votre système.",
|
||||||
"LabelPasswordResetProvider": "Fournisseur de récupération de mot de passe",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2012,5 +2012,7 @@
|
||||||
"MetadataImagesLoadError": "Не удалось загрузить настройки метаданных",
|
"MetadataImagesLoadError": "Не удалось загрузить настройки метаданных",
|
||||||
"HeaderPageNotFound": "Станица не найдена",
|
"HeaderPageNotFound": "Станица не найдена",
|
||||||
"PageNotFound": "Это не та страница, которую вы искали.",
|
"PageNotFound": "Это не та страница, которую вы искали.",
|
||||||
"SettingsPageLoadError": "Не удалось загрузить страницу параметров"
|
"SettingsPageLoadError": "Не удалось загрузить страницу параметров",
|
||||||
|
"RetryWithGlobalSearch": "Повторите попытку с помощью глобального поиска",
|
||||||
|
"StreamCountExceedsLimit": "Количество потоков превышает предельное значение"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2009,5 +2009,7 @@
|
||||||
"MetadataNfoLoadError": "Не вдалося завантажити налаштування метаданих NFO",
|
"MetadataNfoLoadError": "Не вдалося завантажити налаштування метаданих NFO",
|
||||||
"HeaderPageNotFound": "Сторінку не знайдено",
|
"HeaderPageNotFound": "Сторінку не знайдено",
|
||||||
"PageNotFound": "Це не та сторінка, яку ви шукаєте.",
|
"PageNotFound": "Це не та сторінка, яку ви шукаєте.",
|
||||||
"SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань"
|
"SettingsPageLoadError": "Не вдалося завантажити сторінку налаштувань",
|
||||||
|
"StreamCountExceedsLimit": "Кількість потоків перевищує ліміт",
|
||||||
|
"RetryWithGlobalSearch": "Повторити спробу з глобальним пошуком"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue