mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-18 11:28:23 -07:00
Merge branch 'master' into disable-embedded-subs
This commit is contained in:
commit
7912daf0c6
@ -21,10 +21,6 @@ jobs:
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: './bump-version $(JellyfinVersion)'
|
||||
displayName: Bump internal version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||
@ -76,10 +72,6 @@ jobs:
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
|
||||
- script: './bump-version $(JellyfinVersion)'
|
||||
displayName: Bump internal version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
174
.eslintrc.js
174
.eslintrc.js
@ -70,7 +70,92 @@ module.exports = {
|
||||
],
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
|
||||
}
|
||||
},
|
||||
polyfills: [
|
||||
// Native Promises Only
|
||||
'Promise',
|
||||
// whatwg-fetch
|
||||
'fetch',
|
||||
// document-register-element
|
||||
'document.registerElement',
|
||||
// resize-observer-polyfill
|
||||
'ResizeObserver',
|
||||
// fast-text-encoding
|
||||
'TextEncoder',
|
||||
// intersection-observer
|
||||
'IntersectionObserver',
|
||||
// Core-js
|
||||
'Object.assign',
|
||||
'Object.is',
|
||||
'Object.setPrototypeOf',
|
||||
'Object.toString',
|
||||
'Object.freeze',
|
||||
'Object.seal',
|
||||
'Object.preventExtensions',
|
||||
'Object.isFrozen',
|
||||
'Object.isSealed',
|
||||
'Object.isExtensible',
|
||||
'Object.getOwnPropertyDescriptor',
|
||||
'Object.getPrototypeOf',
|
||||
'Object.keys',
|
||||
'Object.entries',
|
||||
'Object.getOwnPropertyNames',
|
||||
'Function.name',
|
||||
'Function.hasInstance',
|
||||
'Array.from',
|
||||
'Array.arrayOf',
|
||||
'Array.copyWithin',
|
||||
'Array.fill',
|
||||
'Array.find',
|
||||
'Array.findIndex',
|
||||
'Array.iterator',
|
||||
'String.fromCodePoint',
|
||||
'String.raw',
|
||||
'String.iterator',
|
||||
'String.codePointAt',
|
||||
'String.endsWith',
|
||||
'String.includes',
|
||||
'String.repeat',
|
||||
'String.startsWith',
|
||||
'String.trim',
|
||||
'String.anchor',
|
||||
'String.big',
|
||||
'String.blink',
|
||||
'String.bold',
|
||||
'String.fixed',
|
||||
'String.fontcolor',
|
||||
'String.fontsize',
|
||||
'String.italics',
|
||||
'String.link',
|
||||
'String.small',
|
||||
'String.strike',
|
||||
'String.sub',
|
||||
'String.sup',
|
||||
'RegExp',
|
||||
'Number',
|
||||
'Math',
|
||||
'Date',
|
||||
'async',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'ArrayBuffer',
|
||||
'DataView',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Reflect',
|
||||
// Temporary while eslint-compat-plugin is buggy
|
||||
'document.querySelector'
|
||||
]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@ -122,93 +207,6 @@ module.exports = {
|
||||
'Windows': 'readonly'
|
||||
},
|
||||
rules: {
|
||||
},
|
||||
settings: {
|
||||
polyfills: [
|
||||
// Native Promises Only
|
||||
'Promise',
|
||||
// whatwg-fetch
|
||||
'fetch',
|
||||
// document-register-element
|
||||
'document.registerElement',
|
||||
// resize-observer-polyfill
|
||||
'ResizeObserver',
|
||||
// fast-text-encoding
|
||||
'TextEncoder',
|
||||
// intersection-observer
|
||||
'IntersectionObserver',
|
||||
// Core-js
|
||||
'Object.assign',
|
||||
'Object.is',
|
||||
'Object.setPrototypeOf',
|
||||
'Object.toString',
|
||||
'Object.freeze',
|
||||
'Object.seal',
|
||||
'Object.preventExtensions',
|
||||
'Object.isFrozen',
|
||||
'Object.isSealed',
|
||||
'Object.isExtensible',
|
||||
'Object.getOwnPropertyDescriptor',
|
||||
'Object.getPrototypeOf',
|
||||
'Object.keys',
|
||||
'Object.entries',
|
||||
'Object.getOwnPropertyNames',
|
||||
'Function.name',
|
||||
'Function.hasInstance',
|
||||
'Array.from',
|
||||
'Array.arrayOf',
|
||||
'Array.copyWithin',
|
||||
'Array.fill',
|
||||
'Array.find',
|
||||
'Array.findIndex',
|
||||
'Array.iterator',
|
||||
'String.fromCodePoint',
|
||||
'String.raw',
|
||||
'String.iterator',
|
||||
'String.codePointAt',
|
||||
'String.endsWith',
|
||||
'String.includes',
|
||||
'String.repeat',
|
||||
'String.startsWith',
|
||||
'String.trim',
|
||||
'String.anchor',
|
||||
'String.big',
|
||||
'String.blink',
|
||||
'String.bold',
|
||||
'String.fixed',
|
||||
'String.fontcolor',
|
||||
'String.fontsize',
|
||||
'String.italics',
|
||||
'String.link',
|
||||
'String.small',
|
||||
'String.strike',
|
||||
'String.sub',
|
||||
'String.sup',
|
||||
'RegExp',
|
||||
'Number',
|
||||
'Math',
|
||||
'Date',
|
||||
'async',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'ArrayBuffer',
|
||||
'DataView',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Reflect',
|
||||
// Temporary while eslint-compat-plugin is buggy
|
||||
'document.querySelector'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
16
.github/dependabot.yaml
vendored
16
.github/dependabot.yaml
vendored
@ -1,16 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: hls.js
|
||||
update-types: [ version-update:semver-major ]
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
33
.github/renovate.json
vendored
Normal file
33
.github/renovate.json
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "development dependencies",
|
||||
"groupSlug": "dev-deps"
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["dependencies"],
|
||||
"groupName": "dependencies",
|
||||
"groupSlug": "deps"
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["action"],
|
||||
"groupName": "CI dependencies",
|
||||
"groupSlug": "ci-deps"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["hls.js"],
|
||||
"matchUpdateTypes": "major",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"dependencyDashboard": false,
|
||||
"ignoreDeps": ["npm", "node"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"enabledManagers": ["npm", "github-actions"],
|
||||
"labels": ["dependencies"],
|
||||
"rebaseWhen": "behind-base-branch",
|
||||
"rangeStrategy": "pin"
|
||||
}
|
27
.github/stale.yml
vendored
27
.github/stale.yml
vendored
@ -1,27 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 21
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- security
|
||||
- roadmap
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
||||
# Disable automatic closing of pull requests
|
||||
pulls:
|
||||
daysUntilClose: false
|
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v2.4.1
|
||||
uses: actions/setup-node@v2.5.1
|
||||
with:
|
||||
node-version: 12
|
||||
check-latest: true
|
||||
@ -26,7 +26,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v2.4.1
|
||||
uses: actions/setup-node@v2.5.1
|
||||
with:
|
||||
node-version: 12
|
||||
check-latest: true
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
@ -89,7 +89,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v2.4.1
|
||||
uses: actions/setup-node@v2.5.1
|
||||
with:
|
||||
node-version: 12
|
||||
check-latest: true
|
||||
@ -99,7 +99,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
27
.github/workflows/repo-stale.yaml
vendored
Normal file
27
.github/workflows/repo-stale.yaml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Issue Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@v4.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
@ -4,9 +4,8 @@ $(info $(shell set -x; if [ "$$(id -u)" = "0" ]; then echo "Installing git"; dnf
|
||||
NAME := jellyfin-web
|
||||
VERSION := $(shell set -x; sed -ne '/^Version:/s/.* *//p' $(DIR)/$(NAME).spec)
|
||||
RELEASE := $(shell set -x; sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/$(NAME).spec)
|
||||
GIT_VER := $(shell set -x; git describe --tags | sed -e 's/^v//' -e 's/-[0-9]*-g.*$$//')
|
||||
SRPM := jellyfin-web-$(subst -,~,$(GIT_VER))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
||||
TARBALL :=$(NAME)-$(subst -,~,$(GIT_VER)).tar.gz
|
||||
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
||||
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
||||
|
||||
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/
|
||||
|
||||
@ -20,9 +19,9 @@ $(DIR)/$(TARBALL):
|
||||
cd $(DIR)/; \
|
||||
SOURCE_DIR=.. \
|
||||
WORKDIR="$${PWD}"; \
|
||||
version=$(GIT_VER); \
|
||||
version=$(VERSION); \
|
||||
tar \
|
||||
--transform "s,^\.,$(NAME)-$(subst -,~,$(GIT_VER))," \
|
||||
--transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
|
||||
--exclude='.git*' \
|
||||
--exclude='**/.git' \
|
||||
--exclude='**/.hg' \
|
||||
@ -34,7 +33,6 @@ $(DIR)/$(TARBALL):
|
||||
-C $${SOURCE_DIR} ./
|
||||
|
||||
$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin-web.spec
|
||||
./bump_version $(GIT_VER)
|
||||
cd $(DIR)/; \
|
||||
rpmbuild -bs $(NAME).spec \
|
||||
--define "_sourcedir $$PWD/" \
|
||||
|
@ -30,6 +30,10 @@ Jellyfin is a free software media system that puts you in control of managing an
|
||||
%build
|
||||
|
||||
%install
|
||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
||||
# Required for CentOS build
|
||||
chown root:root -R .
|
||||
%endif
|
||||
npm ci --no-audit --unsafe-perm
|
||||
%{__mkdir} -p %{buildroot}%{_datadir}
|
||||
mv dist %{buildroot}%{_datadir}/jellyfin-web
|
||||
|
5707
package-lock.json
generated
5707
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
176
package.json
176
package.json
@ -5,98 +5,98 @@
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
"@babel/eslint-plugin": "^7.14.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.0",
|
||||
"@babel/plugin-proposal-private-methods": "^7.16.0",
|
||||
"@babel/plugin-transform-modules-umd": "^7.16.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@uupaa/dynamic-import-polyfill": "^1.0.2",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-dynamic-import-polyfill": "^1.0.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"confusing-browser-globals": "^1.0.10",
|
||||
"copy-webpack-plugin": "^9.1.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"cssnano": "^5.0.10",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-compat": "^3.13.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint-plugin-react": "^7.27.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"expose-loader": "^3.1.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"postcss": "^8.3.11",
|
||||
"postcss-loader": "^6.2.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-scss": "^4.0.2",
|
||||
"sass": "^1.43.4",
|
||||
"sass-loader": "^12.3.0",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"stylelint": "^14.1.0",
|
||||
"stylelint-config-rational-order": "^0.1.2",
|
||||
"stylelint-no-browser-hacks": "^1.2.1",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"stylelint-scss": "^4.0.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.4.4",
|
||||
"webpack": "^5.64.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"workbox-webpack-plugin": "^6.2.4",
|
||||
"worker-plugin": "^5.0.1"
|
||||
"@babel/core": "7.16.7",
|
||||
"@babel/eslint-parser": "7.16.5",
|
||||
"@babel/eslint-plugin": "7.16.5",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-private-methods": "7.16.7",
|
||||
"@babel/plugin-transform-modules-umd": "7.16.7",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||
"@typescript-eslint/parser": "4.33.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"autoprefixer": "10.4.1",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"css-loader": "6.5.1",
|
||||
"cssnano": "5.0.14",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-compat": "4.0.0",
|
||||
"eslint-plugin-eslint-comments": "3.2.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "6.5.1",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-react": "7.28.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"expose-loader": "3.1.0",
|
||||
"html-loader": "3.0.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"postcss": "8.4.5",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "7.2.0",
|
||||
"postcss-scss": "4.0.2",
|
||||
"sass": "1.45.2",
|
||||
"sass-loader": "12.4.0",
|
||||
"source-map-loader": "3.0.1",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.2.0",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "1.2.1",
|
||||
"stylelint-order": "5.0.0",
|
||||
"stylelint-scss": "4.1.0",
|
||||
"ts-loader": "9.2.6",
|
||||
"typescript": "4.5.4",
|
||||
"webpack": "5.65.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "4.7.2",
|
||||
"webpack-merge": "5.8.0",
|
||||
"workbox-webpack-plugin": "6.2.4",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "^4.5.1",
|
||||
"@fontsource/noto-sans-hk": "^4.5.2",
|
||||
"@fontsource/noto-sans-jp": "^4.5.2",
|
||||
"@fontsource/noto-sans-kr": "^4.5.2",
|
||||
"@fontsource/noto-sans-sc": "^4.5.2",
|
||||
"blurhash": "^1.1.4",
|
||||
"@fontsource/noto-sans": "4.5.1",
|
||||
"@fontsource/noto-sans-hk": "4.5.2",
|
||||
"@fontsource/noto-sans-jp": "4.5.2",
|
||||
"@fontsource/noto-sans-kr": "4.5.2",
|
||||
"@fontsource/noto-sans-sc": "4.5.2",
|
||||
"blurhash": "1.1.4",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "^3.19.1",
|
||||
"date-fns": "^2.25.0",
|
||||
"dompurify": "^2.3.3",
|
||||
"epubjs": "^0.3.90",
|
||||
"fast-text-encoding": "^1.0.3",
|
||||
"flv.js": "^1.6.2",
|
||||
"headroom.js": "^0.12.0",
|
||||
"hls.js": "^0.14.17",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"jellyfin-apiclient": "^1.9.1",
|
||||
"jquery": "^3.5.1",
|
||||
"jstree": "^3.3.12",
|
||||
"libarchive.js": "^1.3.0",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.20.2",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.4",
|
||||
"epubjs": "0.3.90",
|
||||
"fast-text-encoding": "1.0.3",
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"hls.js": "0.14.17",
|
||||
"intersection-observer": "0.12.0",
|
||||
"jellyfin-apiclient": "1.10.0",
|
||||
"jquery": "3.6.0",
|
||||
"jstree": "3.3.12",
|
||||
"libarchive.js": "1.3.0",
|
||||
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.0.3",
|
||||
"material-design-icons-iconfont": "^6.1.1",
|
||||
"native-promise-only": "^0.8.0-a",
|
||||
"page": "^1.11.6",
|
||||
"pdfjs-dist": "2.6.347",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^6.0.0",
|
||||
"sortablejs": "^1.14.0",
|
||||
"swiper": "^6.8.4",
|
||||
"webcomponents.js": "^0.7.24",
|
||||
"whatwg-fetch": "^3.6.2",
|
||||
"workbox-core": "^6.2.4",
|
||||
"workbox-precaching": "^6.2.4"
|
||||
"lodash-es": "4.17.21",
|
||||
"marked": "4.0.10",
|
||||
"material-design-icons-iconfont": "6.1.1",
|
||||
"native-promise-only": "0.8.1",
|
||||
"page": "1.11.6",
|
||||
"pdfjs-dist": "2.12.313",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.0",
|
||||
"sortablejs": "1.14.0",
|
||||
"swiper": "6.8.4",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.2",
|
||||
"workbox-core": "6.2.4",
|
||||
"workbox-precaching": "6.2.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
|
1
src/assets/img/devices/apple.svg
Normal file
1
src/assets/img/devices/apple.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" fill="#fff"/></svg>
|
After Width: | Height: | Size: 663 B |
@ -280,6 +280,16 @@ import 'material-design-icons-iconfont';
|
||||
element.removeEventListener(name, fn);
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
}
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') === 0);
|
||||
}
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
|
@ -797,6 +797,10 @@ class AppRouter {
|
||||
return '#!/list.html?type=Programs&IsAiring=true&serverId=' + options.serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'channels') {
|
||||
return '#!/livetv.html?tab=2&serverId=' + options.serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'dvrschedule') {
|
||||
return '#!/livetv.html?tab=4&serverId=' + options.serverId;
|
||||
}
|
||||
|
@ -199,7 +199,6 @@ const supportedFeatures = function () {
|
||||
if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
|
||||
features.push('exit');
|
||||
} else {
|
||||
features.push('exitmenu');
|
||||
features.push('plugins');
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,9 @@ import layoutManager from './layoutManager';
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
} else if (activeElement.classList.contains('btnPlay')) {
|
||||
// Resume has priority over Play
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnResume')));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
|
@ -333,6 +333,7 @@ button::-moz-focus-inner {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.innerCardFooter > .cardText {
|
||||
@ -355,7 +356,8 @@ button::-moz-focus-inner {
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.cardTextCentered {
|
||||
.cardTextCentered,
|
||||
.cardTextCentered > .textActionButton {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -771,6 +771,7 @@ import ServerConnections from '../ServerConnections';
|
||||
* @returns {string} HTML markup of the card's footer text element.
|
||||
*/
|
||||
function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) {
|
||||
item = item.ProgramInfo || item;
|
||||
let html = '';
|
||||
|
||||
if (logoUrl) {
|
||||
@ -855,6 +856,10 @@ import ServerConnections from '../ServerConnections';
|
||||
}
|
||||
}
|
||||
|
||||
if (item.ExtraType && item.ExtraType !== 'Unknown') {
|
||||
lines.push(globalize.translate(item.ExtraType));
|
||||
}
|
||||
|
||||
if (options.showItemCounts) {
|
||||
lines.push(getItemCountsHtml(options, item));
|
||||
}
|
||||
|
60
src/components/dashboard/users/AccessScheduleList.tsx
Normal file
60
src/components/dashboard/users/AccessScheduleList.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import datetime from '../../../scripts/datetime';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createButtonElement = ({index}) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='btnDelete listItemButton'
|
||||
data-index='${index}'
|
||||
>
|
||||
<span class='material-icons delete' />
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
index: number;
|
||||
Id: number;
|
||||
DayOfWeek?: string;
|
||||
StartHour?: number ;
|
||||
EndHour?: number;
|
||||
}
|
||||
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
|
||||
if (pct) {
|
||||
minutes = Math.floor(60 * pct);
|
||||
}
|
||||
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='liSchedule listItem'
|
||||
data-day={ DayOfWeek}
|
||||
data-start={ StartHour}
|
||||
data-end={ EndHour}
|
||||
>
|
||||
<div className='listItemBody two-line'>
|
||||
<h3 className='listItemBodyText'>
|
||||
{globalize.translate(DayOfWeek)}
|
||||
</h3>
|
||||
<div className='listItemBodyText secondary'>
|
||||
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
index: index
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessScheduleList;
|
38
src/components/dashboard/users/BlockedTagList.tsx
Normal file
38
src/components/dashboard/users/BlockedTagList.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
const createButtonElement = ({tag}) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='blockedTag btnDeleteTag listItemButton'
|
||||
data-tag='${tag}'
|
||||
>
|
||||
<span class='material-icons delete' />
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
|
||||
return (
|
||||
<div className='paperList'>
|
||||
<div className='listItem'>
|
||||
<div className='listItemBody'>
|
||||
<h3 className='listItemBodyText'>
|
||||
{tag}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
tag: tag
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockedTagList;
|
@ -1,8 +1,8 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createCheckBoxElement = ({ type, className, title }) => ({
|
||||
__html: `<label>
|
||||
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({
|
||||
__html: `<label class="${labelClassName}">
|
||||
<input
|
||||
is="emby-checkbox"
|
||||
type="${type}"
|
||||
@ -13,15 +13,18 @@ const createCheckBoxElement = ({ type, className, title }) => ({
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
labelClassName?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
title?: string
|
||||
}
|
||||
|
||||
const CheckBoxElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
|
||||
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, type, className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
labelClassName: labelClassName ? labelClassName : '',
|
||||
type: type,
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
|
@ -4,27 +4,33 @@ type IProps = {
|
||||
className?: string;
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
ItemType?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string;
|
||||
}
|
||||
|
||||
const createCheckBoxElement = ({className, Name, Id}) => ({
|
||||
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}) => ({
|
||||
__html: `<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
is="emby-checkbox"
|
||||
class="${className}"
|
||||
data-id="${Id}"
|
||||
${dataAttributes} ${checkedAttribute}
|
||||
/>
|
||||
<span>${Name}</span>
|
||||
<span>${Name} ${AppName}</span>
|
||||
</label>`
|
||||
});
|
||||
|
||||
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id}: IProps) => {
|
||||
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
className: className,
|
||||
Name: Name,
|
||||
Id: Id
|
||||
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
|
||||
AppName: AppName ? `- ${AppName}` : '',
|
||||
checkedAttribute: checkedAttribute
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
30
src/components/dashboard/users/LinkEditUserPreferences.tsx
Normal file
30
src/components/dashboard/users/LinkEditUserPreferences.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const createLinkElement = ({ className, title }) => ({
|
||||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="${className}"
|
||||
href='#'
|
||||
>
|
||||
${title}
|
||||
</a>`
|
||||
});
|
||||
|
||||
const LinkEditUserPreferences: FunctionComponent<IProps> = ({ className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkEditUserPreferences;
|
52
src/components/dashboard/users/SectionTabs.tsx
Normal file
52
src/components/dashboard/users/SectionTabs.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
const createLinkElement = ({ activeTab }) => ({
|
||||
__html: `<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('useredit.html', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
|
||||
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
data-role='controlgroup'
|
||||
data-type='horizontal'
|
||||
className='localnav'
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
activeTab: activeTab
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTabs;
|
@ -1,23 +1,24 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
title: string;
|
||||
className?: string;
|
||||
icon: string,
|
||||
}
|
||||
|
||||
const createButtonElement = ({ className, title, icon }) => ({
|
||||
__html: `<button
|
||||
is="emby-button"
|
||||
type="button"
|
||||
class="${className}"
|
||||
style="margin-left:1em;"
|
||||
title="${title}">
|
||||
title="${title}"
|
||||
>
|
||||
<span class="material-icons ${icon}"></span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
icon?: string,
|
||||
}
|
||||
|
||||
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
|
43
src/components/dashboard/users/SelectElement.tsx
Normal file
43
src/components/dashboard/users/SelectElement.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
label="${label}"
|
||||
>
|
||||
${option}
|
||||
</select>`
|
||||
});
|
||||
|
||||
type ProvidersArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
}
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
label?: string;
|
||||
currentProviderId: string;
|
||||
providers: ProvidersArr[]
|
||||
}
|
||||
|
||||
const SelectElement: FunctionComponent<IProps> = ({ className, label, currentProviderId, providers }: IProps) => {
|
||||
const renderOption = providers.map((provider) => {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectElement;
|
46
src/components/dashboard/users/SelectMaxParentalRating.tsx
Normal file
46
src/components/dashboard/users/SelectMaxParentalRating.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
label="${label}"
|
||||
>
|
||||
<option value=''></option>
|
||||
${option}
|
||||
</select>`
|
||||
});
|
||||
|
||||
type RatingsArr = {
|
||||
Name: string;
|
||||
Value: number;
|
||||
}
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
label?: string;
|
||||
parentalRatings: RatingsArr[];
|
||||
}
|
||||
|
||||
const SelectMaxParentalRating: FunctionComponent<IProps> = ({ className, label, parentalRatings }: IProps) => {
|
||||
const renderOption = () => {
|
||||
let content = '';
|
||||
for (const rating of parentalRatings) {
|
||||
content += `<option value='${rating.Value}'>${rating.Name}</option>`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption()
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMaxParentalRating;
|
@ -0,0 +1,35 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, id, label }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
id="${id}"
|
||||
label="${label}"
|
||||
>
|
||||
<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>
|
||||
<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>
|
||||
<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>
|
||||
</select>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
id?: string;
|
||||
label?: string
|
||||
}
|
||||
|
||||
const SelectSyncPlayAccessElement: FunctionComponent<IProps> = ({ className, id, label }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
id: id,
|
||||
label: globalize.translate(label)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectSyncPlayAccessElement;
|
@ -32,15 +32,11 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (path === 'Network') {
|
||||
promises.push(ApiClient.getNetworkDevices());
|
||||
if (path) {
|
||||
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
||||
promises.push(ApiClient.getParentPath(path));
|
||||
} else {
|
||||
if (path) {
|
||||
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
||||
promises.push(ApiClient.getParentPath(path));
|
||||
} else {
|
||||
promises.push(ApiClient.getDrives());
|
||||
}
|
||||
promises.push(ApiClient.getDrives());
|
||||
}
|
||||
|
||||
Promise.all(promises).then(
|
||||
@ -61,10 +57,6 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
||||
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
html += getItem('lnkPath lnkDirectory', '', 'Network', globalize.translate('ButtonNetwork'));
|
||||
}
|
||||
|
||||
page.querySelector('.results').innerHTML = html;
|
||||
loading.hide();
|
||||
}, () => {
|
||||
|
@ -532,6 +532,11 @@ import ServerConnections from '../ServerConnections';
|
||||
section: 'guide'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'channels'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||
|
16
src/components/images/blurhash.worker.ts
Normal file
16
src/components/images/blurhash.worker.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
self.onmessage = ({ data: { hash, width, height } }): void => {
|
||||
try {
|
||||
self.postMessage({
|
||||
pixels: decode(hash, width, height),
|
||||
hsh: hash,
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
} catch {
|
||||
throw new TypeError(`Blurhash ${hash} is not valid`);
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-restricted-globals */
|
@ -1,7 +1,22 @@
|
||||
import Worker from './blurhash.worker.ts'; // eslint-disable-line import/default
|
||||
import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import { decode, isBlurhashValid } from 'blurhash';
|
||||
import './style.scss';
|
||||
// eslint-disable-next-line compat/compat
|
||||
const worker = new Worker();
|
||||
const targetDic = {};
|
||||
worker.addEventListener(
|
||||
'message',
|
||||
({ data: { pixels, hsh, width, height } }) => {
|
||||
const elems = targetDic[hsh];
|
||||
if (elems && elems.length) {
|
||||
for (const elem of elems) {
|
||||
drawBlurhash(elem, pixels, width, height);
|
||||
}
|
||||
delete targetDic[hsh];
|
||||
}
|
||||
}
|
||||
);
|
||||
/* eslint-disable indent */
|
||||
|
||||
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
||||
@ -12,42 +27,45 @@ import './style.scss';
|
||||
fillImageElement(elem, source);
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, blurhashstr) {
|
||||
if (isBlurhashValid(blurhashstr)) {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us,
|
||||
function drawBlurhash(target, pixels, width, height) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// This class is just an utility class, so users can customize the canvas using their own CSS.
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
});
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, hash) {
|
||||
try {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
|
||||
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
|
||||
// Lower values had more visible pixelation
|
||||
const width = 18;
|
||||
const height = 18;
|
||||
let pixels;
|
||||
try {
|
||||
pixels = decode(blurhashstr, width, height);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decode error: ', err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
const width = 20;
|
||||
const height = 20;
|
||||
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
|
||||
targetDic[hash].push(target);
|
||||
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
canvas.classList.add('lazy-blurhash-fadein-fast');
|
||||
} else {
|
||||
canvas.classList.add('lazy-blurhash-fadein');
|
||||
}
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
worker.postMessage({
|
||||
hash,
|
||||
width,
|
||||
height
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,14 +83,25 @@ import './style.scss';
|
||||
}
|
||||
|
||||
if (entry.intersectionRatio > 0) {
|
||||
if (source) fillImageElement(target, source);
|
||||
if (source) {
|
||||
fillImageElement(target, source);
|
||||
}
|
||||
} else if (!source) {
|
||||
requestAnimationFrame(() => {
|
||||
emptyImageElement(target);
|
||||
});
|
||||
emptyImageElement(target);
|
||||
}
|
||||
}
|
||||
|
||||
function onAnimationEnd(event) {
|
||||
const elem = event.target;
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = elem.previousSibling;
|
||||
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
});
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
}
|
||||
|
||||
function fillImageElement(elem, url) {
|
||||
if (url === undefined) {
|
||||
throw new TypeError('url cannot be undefined');
|
||||
@ -82,6 +111,7 @@ import './style.scss';
|
||||
preloaderImg.src = url;
|
||||
|
||||
elem.classList.add('lazy-hidden');
|
||||
elem.addEventListener('animationend', onAnimationEnd);
|
||||
|
||||
preloaderImg.addEventListener('load', () => {
|
||||
requestAnimationFrame(() => {
|
||||
@ -92,25 +122,22 @@ import './style.scss';
|
||||
}
|
||||
elem.removeAttribute('data-src');
|
||||
|
||||
elem.classList.remove('lazy-hidden');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
elem.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
elem.classList.add('lazy-image-fadein');
|
||||
}
|
||||
|
||||
const canvas = elem.previousSibling;
|
||||
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
elem.classList.remove('lazy-hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function emptyImageElement(elem) {
|
||||
// block repeated call - requestAnimationFrame twice for one image
|
||||
if (elem.getAttribute('data-src')) return;
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
@ -125,16 +152,6 @@ import './style.scss';
|
||||
|
||||
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
elem.classList.add('lazy-hidden');
|
||||
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
canvas.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
canvas.classList.add('lazy-image-fadein');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function lazyChildren(elem) {
|
||||
|
@ -1,17 +1,3 @@
|
||||
.lazy-image-fadein {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.lazy-image-fadein-fast {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.lazy-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
@ -22,12 +8,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lazy-blurhash-fadein-fast {
|
||||
.lazy-image-fadein {
|
||||
opacity: 1;
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.lazy-image-fadein-fast {
|
||||
opacity: 1;
|
||||
animation: fadein 0.1s;
|
||||
}
|
||||
|
||||
.lazy-blurhash-fadein {
|
||||
animation: fadein 0.4s;
|
||||
.lazy-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.blurhash-canvas {
|
||||
|
@ -20,7 +20,7 @@ export function getDisplayName(item, options = {}) {
|
||||
}
|
||||
if (item.Type === 'Episode' && item.ParentIndexNumber === 0) {
|
||||
name = globalize.translate('ValueSpecialEpisodeName', name);
|
||||
} else if ((item.Type === 'Episode' || item.Type === 'Program') && item.IndexNumber != null && item.ParentIndexNumber != null && options.includeIndexNumber !== false) {
|
||||
} else if ((item.Type === 'Episode' || item.Type === 'Program' || item.Type === 'Recording') && item.IndexNumber != null && item.ParentIndexNumber != null && options.includeIndexNumber !== false) {
|
||||
let displayIndexNumber = item.IndexNumber;
|
||||
|
||||
let number = displayIndexNumber;
|
||||
|
@ -143,10 +143,8 @@ import template from './itemMediaInfo.template.html';
|
||||
if (stream.NalLengthSize) {
|
||||
attributes.push(createAttribute('NAL', stream.NalLengthSize));
|
||||
}
|
||||
if (stream.Type !== 'Video') {
|
||||
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Type === 'Subtitle') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
|
||||
}
|
||||
|
@ -13,7 +13,10 @@
|
||||
callback(entry);
|
||||
});
|
||||
},
|
||||
{rootMargin: '25%'});
|
||||
{
|
||||
rootMargin: '50%',
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
this.observer = observer;
|
||||
}
|
||||
|
@ -417,7 +417,7 @@ import template from './libraryoptionseditor.template.html';
|
||||
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies');
|
||||
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies' && contentType !== 'mixed');
|
||||
|
||||
return populateMetadataSettings(parent, contentType);
|
||||
}
|
||||
|
@ -164,6 +164,10 @@
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.listItemImage .cardImageIcon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
@media all and (max-width: 64em) {
|
||||
.listItemImage-large {
|
||||
width: 22vw;
|
||||
|
@ -73,7 +73,7 @@ import template from './mediaLibraryCreator.template.html';
|
||||
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
|
||||
const value = this.value;
|
||||
const dlg = $(this).parents('.dialog')[0];
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value == 'mixed' ? '' : value);
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
|
||||
|
||||
if (value) {
|
||||
dlg.querySelector('.libraryOptions').classList.remove('hide');
|
||||
|
@ -131,7 +131,8 @@ import '../../elements/emby-button/emby-button';
|
||||
if ((item.Type === 'Episode' || item.MediaType === 'Photo') && options.originalAirDate !== false) {
|
||||
if (item.PremiereDate) {
|
||||
try {
|
||||
date = datetime.parseISO8601Date(item.PremiereDate);
|
||||
//don't modify date to locale if episode. Only Dates (not times) are stored, or editable in the edit metadata dialog
|
||||
date = datetime.parseISO8601Date(item.PremiereDate, item.Type !== 'Episode');
|
||||
|
||||
text = datetime.toLocaleDateString(date);
|
||||
miscInfo.push(text);
|
||||
|
@ -175,6 +175,12 @@ import itemHelper from '../itemHelper';
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), selectedItems[0]).then(firstItem => {
|
||||
const menuItems = [];
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('SelectAll'),
|
||||
id: 'selectall',
|
||||
icon: 'select_all'
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('AddToCollection'),
|
||||
id: 'addtocollection',
|
||||
@ -246,6 +252,19 @@ import itemHelper from '../itemHelper';
|
||||
const serverId = apiClient.serverInfo().Id;
|
||||
|
||||
switch (id) {
|
||||
case 'selectall':
|
||||
{
|
||||
const elems = document.querySelectorAll('.itemSelectionPanel');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const chkItemSelect = elems[i].querySelector('.chkItemSelect');
|
||||
|
||||
if (chkItemSelect && !chkItemSelect.classList.contains('checkedInitial') && !chkItemSelect.checked && chkItemSelect.getBoundingClientRect().width != 0) {
|
||||
chkItemSelect.checked = true;
|
||||
updateItemSelection(chkItemSelect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'addtocollection':
|
||||
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
||||
new collectionEditor({
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
@ -3,6 +3,8 @@ import { playbackManager } from '../playback/playbackmanager';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
||||
import NotificationIcon from './notificationicon.png';
|
||||
|
||||
function onOneDocumentClick() {
|
||||
document.removeEventListener('click', onOneDocumentClick);
|
||||
document.removeEventListener('keydown', onOneDocumentClick);
|
||||
@ -71,8 +73,8 @@ function showNotification(options, timeoutMs, apiClient) {
|
||||
|
||||
options.data = options.data || {};
|
||||
options.data.serverId = apiClient.serverInfo().Id;
|
||||
options.icon = options.icon || getIconUrl();
|
||||
options.badge = options.badge || getIconUrl('badge.png');
|
||||
options.icon = options.icon || NotificationIcon;
|
||||
options.badge = options.badge || NotificationIcon;
|
||||
|
||||
resetRegistration();
|
||||
|
||||
@ -148,11 +150,6 @@ function onLibraryChanged(data, apiClient) {
|
||||
});
|
||||
}
|
||||
|
||||
function getIconUrl(name) {
|
||||
name = name || 'notificationicon.png';
|
||||
return './components/notifications/' + name;
|
||||
}
|
||||
|
||||
function showPackageInstallNotification(apiClient, installation, status) {
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
if (!user.Policy.IsAdministrator) {
|
||||
@ -180,7 +177,7 @@ function showPackageInstallNotification(apiClient, installation, status) {
|
||||
{
|
||||
action: 'cancel-install',
|
||||
title: globalize.translate('ButtonCancel'),
|
||||
icon: getIconUrl()
|
||||
icon: NotificationIcon
|
||||
}
|
||||
];
|
||||
|
||||
@ -249,7 +246,7 @@ Events.on(serverNotifications, 'RestartRequired', function (e, apiClient) {
|
||||
{
|
||||
action: 'restart',
|
||||
title: globalize.translate('Restart'),
|
||||
icon: getIconUrl()
|
||||
icon: NotificationIcon
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -68,7 +68,6 @@ const NewUserPage: FunctionComponent = () => {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||
// eslint-disable-next-line compat/compat
|
||||
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
||||
loadMediaFolders(responses[0].Items);
|
||||
loadChannels(responses[1].Items);
|
||||
@ -191,6 +190,7 @@ const NewUserPage: FunctionComponent = () => {
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -219,6 +219,7 @@ const NewUserPage: FunctionComponent = () => {
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
526
src/components/pages/UserEditPage.tsx
Normal file
526
src/components/pages/UserEditPage.tsx
Normal file
@ -0,0 +1,526 @@
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../scripts/clientUtils';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appRouter } from '../appRouter';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import InputElement from '../dashboard/users/InputElement';
|
||||
import LinkEditUserPreferences from '../dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SelectElement from '../dashboard/users/SelectElement';
|
||||
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import loading from '../loading/loading';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
|
||||
const UserEditPage: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]);
|
||||
const [ authProviders, setAuthProviders ] = useState([]);
|
||||
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
|
||||
|
||||
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
|
||||
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const triggerChange = (select) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
const userId = appRouter.param('userId');
|
||||
return window.ApiClient.getUser(userId);
|
||||
};
|
||||
|
||||
const loadAuthProviders = useCallback((user, providers) => {
|
||||
const fldSelectLoginProvider = element?.current?.querySelector('.fldSelectLoginProvider');
|
||||
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
setAuthenticationProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadPasswordResetProviders = useCallback((user, providers) => {
|
||||
const fldSelectPasswordResetProvider = element?.current?.querySelector('.fldSelectPasswordResetProvider');
|
||||
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
setPasswordResetProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadDeleteFolders = useCallback((user, mediaFolders) => {
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
let isChecked;
|
||||
let checkedAttribute;
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
for (const folder of channelsResult.Items) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setDeleteFoldersAccess(itemsArr);
|
||||
|
||||
const chkEnableDeleteAllFolders = element.current.querySelector('.chkEnableDeleteAllFolders');
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user) => {
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(user, providers);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(user, providers);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(user, folders.Items);
|
||||
});
|
||||
|
||||
const disabledUserBanner = element?.current?.querySelector('.disabledUserBanner');
|
||||
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
|
||||
|
||||
const txtUserName = element?.current?.querySelector('#txtUserName');
|
||||
txtUserName.disabled = '';
|
||||
txtUserName.removeAttribute('disabled');
|
||||
|
||||
const lnkEditUserPreferences = element?.current?.querySelector('.lnkEditUserPreferences');
|
||||
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
setUserName(user.Name);
|
||||
element.current.querySelector('#txtUserName').value = user.Name;
|
||||
element.current.querySelector('.chkIsAdmin').checked = user.Policy.IsAdministrator;
|
||||
element.current.querySelector('.chkDisabled').checked = user.Policy.IsDisabled;
|
||||
element.current.querySelector('.chkIsHidden').checked = user.Policy.IsHidden;
|
||||
element.current.querySelector('.chkRemoteControlSharedDevices').checked = user.Policy.EnableSharedDeviceControl;
|
||||
element.current.querySelector('.chkEnableRemoteControlOtherUsers').checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||
element.current.querySelector('.chkEnableDownloading').checked = user.Policy.EnableContentDownloading;
|
||||
element.current.querySelector('.chkManageLiveTv').checked = user.Policy.EnableLiveTvManagement;
|
||||
element.current.querySelector('.chkEnableLiveTvAccess').checked = user.Policy.EnableLiveTvAccess;
|
||||
element.current.querySelector('.chkEnableMediaPlayback').checked = user.Policy.EnableMediaPlayback;
|
||||
element.current.querySelector('.chkEnableAudioPlaybackTranscoding').checked = user.Policy.EnableAudioPlaybackTranscoding;
|
||||
element.current.querySelector('.chkEnableVideoPlaybackTranscoding').checked = user.Policy.EnableVideoPlaybackTranscoding;
|
||||
element.current.querySelector('.chkEnableVideoPlaybackRemuxing').checked = user.Policy.EnablePlaybackRemuxing;
|
||||
element.current.querySelector('.chkForceRemoteSourceTranscoding').checked = user.Policy.ForceRemoteSourceTranscoding;
|
||||
element.current.querySelector('.chkRemoteAccess').checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
|
||||
element.current.querySelector('#txtRemoteClientBitrateLimit').value = user.Policy.RemoteClientBitrateLimit / 1e6 || '';
|
||||
element.current.querySelector('#txtLoginAttemptsBeforeLockout').value = user.Policy.LoginAttemptsBeforeLockout || '0';
|
||||
element.current.querySelector('#txtMaxActiveSessions').value = user.Policy.MaxActiveSessions || '0';
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
element.current.querySelector('#selectSyncPlayAccess').value = user.Policy.SyncPlayAccess;
|
||||
}
|
||||
loading.hide();
|
||||
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(user);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Name = element?.current?.querySelector('#txtUserName').value;
|
||||
user.Policy.IsAdministrator = element?.current?.querySelector('.chkIsAdmin').checked;
|
||||
user.Policy.IsHidden = element?.current?.querySelector('.chkIsHidden').checked;
|
||||
user.Policy.IsDisabled = element?.current?.querySelector('.chkDisabled').checked;
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = element?.current?.querySelector('.chkEnableRemoteControlOtherUsers').checked;
|
||||
user.Policy.EnableLiveTvManagement = element?.current?.querySelector('.chkManageLiveTv').checked;
|
||||
user.Policy.EnableLiveTvAccess = element?.current?.querySelector('.chkEnableLiveTvAccess').checked;
|
||||
user.Policy.EnableSharedDeviceControl = element?.current?.querySelector('.chkRemoteControlSharedDevices').checked;
|
||||
user.Policy.EnableMediaPlayback = element?.current?.querySelector('.chkEnableMediaPlayback').checked;
|
||||
user.Policy.EnableAudioPlaybackTranscoding = element?.current?.querySelector('.chkEnableAudioPlaybackTranscoding').checked;
|
||||
user.Policy.EnableVideoPlaybackTranscoding = element?.current?.querySelector('.chkEnableVideoPlaybackTranscoding').checked;
|
||||
user.Policy.EnablePlaybackRemuxing = element?.current?.querySelector('.chkEnableVideoPlaybackRemuxing').checked;
|
||||
user.Policy.ForceRemoteSourceTranscoding = element?.current?.querySelector('.chkForceRemoteSourceTranscoding').checked;
|
||||
user.Policy.EnableContentDownloading = element?.current?.querySelector('.chkEnableDownloading').checked;
|
||||
user.Policy.EnableRemoteAccess = element?.current?.querySelector('.chkRemoteAccess').checked;
|
||||
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat(element?.current?.querySelector('#txtRemoteClientBitrateLimit').value || '0'));
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt(element?.current?.querySelector('#txtLoginAttemptsBeforeLockout').value || '0');
|
||||
user.Policy.MaxActiveSessions = parseInt(element?.current?.querySelector('#txtMaxActiveSessions').value || '0');
|
||||
user.Policy.AuthenticationProviderId = element?.current?.querySelector('.selectLoginProvider').value;
|
||||
user.Policy.PasswordResetProviderId = element?.current?.querySelector('.selectPasswordResetProvider').value;
|
||||
user.Policy.EnableContentDeletion = element?.current?.querySelector('.chkEnableDeleteAllFolders').checked;
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = element?.current?.querySelector('#selectSyncPlayAccess').value;
|
||||
}
|
||||
window.ApiClient.updateUser(user).then(function () {
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.chkEnableDeleteAllFolders').addEventListener('change', function (this: HTMLInputElement) {
|
||||
if (this.checked) {
|
||||
element?.current?.querySelector('.deleteAccess').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.deleteAccess').classList.remove('hide');
|
||||
}
|
||||
});
|
||||
|
||||
window.ApiClient.getServerConfiguration().then(function (config) {
|
||||
const fldRemoteAccess = element?.current?.querySelector('.fldRemoteAccess');
|
||||
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.editUserProfileForm').addEventListener('submit', onSubmit);
|
||||
|
||||
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
|
||||
window.history.back();
|
||||
});
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTabs activeTab='useredit'/>
|
||||
<div
|
||||
className='lnkEditUserPreferencesContainer'
|
||||
style={{paddingBottom: '1em'}}
|
||||
>
|
||||
<LinkEditUserPreferences
|
||||
className= 'lnkEditUserPreferences button-link'
|
||||
title= 'ButtonEditOtherUserPreferences'
|
||||
/>
|
||||
</div>
|
||||
<form className='editUserProfileForm'>
|
||||
<div className='disabledUserBanner hide'>
|
||||
<div className='btn btnDarkAccent btnStatic'>
|
||||
<div>
|
||||
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
|
||||
</div>
|
||||
<div style={{marginTop: 5}}>
|
||||
{globalize.translate('MessageReenableUser')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='fldUserName' className='inputContainer'>
|
||||
<InputElement
|
||||
type='text'
|
||||
id='txtUserName'
|
||||
label='LabelName'
|
||||
options={'required'}
|
||||
/>
|
||||
</div>
|
||||
<div className='selectContainer fldSelectLoginProvider hide'>
|
||||
<SelectElement
|
||||
className= 'selectLoginProvider'
|
||||
label= 'LabelAuthProvider'
|
||||
currentProviderId={authenticationProviderId}
|
||||
providers={authProviders}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('AuthProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='selectContainer fldSelectPasswordResetProvider hide'>
|
||||
<SelectElement
|
||||
className= 'selectPasswordResetProvider'
|
||||
label= 'LabelPasswordResetProvider'
|
||||
currentProviderId={passwordResetProviderId}
|
||||
providers={passwordResetProviders}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('PasswordResetProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkRemoteAccess'
|
||||
title='AllowRemoteAccess'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('AllowRemoteAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkIsAdmin'
|
||||
title='OptionAllowUserToManageServer'
|
||||
/>
|
||||
<div id='featureAccessFields' className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderFeatureAccess')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableLiveTvAccess'
|
||||
title='OptionAllowBrowsingLiveTv'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkManageLiveTv'
|
||||
title='OptionAllowManageLiveTv'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderPlayback')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableMediaPlayback'
|
||||
title='OptionAllowMediaPlayback'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableAudioPlaybackTranscoding'
|
||||
title='OptionAllowAudioPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableVideoPlaybackTranscoding'
|
||||
title='OptionAllowVideoPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableVideoPlaybackRemuxing'
|
||||
title='OptionAllowVideoPlaybackRemuxing'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkForceRemoteSourceTranscoding'
|
||||
title='OptionForceRemoteSourceTranscoding'
|
||||
/>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtRemoteClientBitrateLimit'
|
||||
label='LabelRemoteClientBitrateLimit'
|
||||
options={'inputMode="decimal" pattern="[0-9]*(.[0-9]+)?" min="{0}" step=".25"'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<div className='selectContainer fldSelectSyncPlayAccess'>
|
||||
<SelectSyncPlayAccessElement
|
||||
className='selectSyncPlayAccess'
|
||||
id='selectSyncPlayAccess'
|
||||
label='LabelSyncPlayAccess'
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('SyncPlayAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel' style={{marginBottom: '1em'}}>
|
||||
{globalize.translate('HeaderAllowMediaDeletionFrom')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableDeleteAllFolders'
|
||||
title='AllLibraries'
|
||||
/>
|
||||
<div className='deleteAccess'>
|
||||
{deleteFoldersAccess.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderRemoteControl')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableRemoteControlOtherUsers'
|
||||
title='OptionAllowRemoteControlOthers'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkRemoteControlSharedDevices'
|
||||
title='OptionAllowRemoteSharedDevices'
|
||||
/>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('Other')}
|
||||
</h2>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableDownloading'
|
||||
title='OptionAllowContentDownload'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionAllowContentDownloadHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkDisabled'
|
||||
title='OptionDisableUser'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionDisableUserHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkIsHidden'
|
||||
title='OptionHideUser'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionHideUserFromLoginHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer' id='fldLoginAttemptsBeforeLockout'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtLoginAttemptsBeforeLockout'
|
||||
label='LabelUserLoginAttemptsBeforeLockout'
|
||||
options={'min={-1} step={1}'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionLoginAttemptsBeforeLockout')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer' id='fldMaxActiveSessions'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtMaxActiveSessions'
|
||||
label='LabelUserMaxActiveSessions'
|
||||
options={'min={0} step={1}'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionMaxActiveSessions')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionMaxActiveSessionsHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
<ButtonElement
|
||||
type='button'
|
||||
className='raised button-cancel block btnCancel'
|
||||
title='ButtonCancel'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserEditPage;
|
317
src/components/pages/UserLibraryAccessPage.tsx
Normal file
317
src/components/pages/UserLibraryAccessPage.tsx
Normal file
@ -0,0 +1,317 @@
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../appRouter';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
}
|
||||
|
||||
const UserLibraryAccessPage: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [channelsItems, setChannelsItems] = useState([]);
|
||||
const [mediaFoldersItems, setMediaFoldersItems] = useState([]);
|
||||
const [devicesItems, setDevicesItems] = useState([]);
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const triggerChange = (select) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((user, mediaFolders) => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setMediaFoldersItems(itemsArr);
|
||||
|
||||
const chkEnableAllFolders = element.current.querySelector('.chkEnableAllFolders');
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((user, channels) => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of channels) {
|
||||
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setChannelsItems(itemsArr);
|
||||
|
||||
if (channels.length) {
|
||||
element?.current?.querySelector('.channelAccessContainer').classList.remove('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.channelAccessContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = element.current.querySelector('.chkEnableAllChannels');
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}, []);
|
||||
|
||||
const loadDevices = useCallback((user, devices) => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const isChecked = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: device.Id,
|
||||
Name: device.Name,
|
||||
AppName : device.AppName,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setDevicesItems(itemsArr);
|
||||
|
||||
const chkEnableAllDevices = element.current.querySelector('.chkEnableAllDevices');
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
element?.current?.querySelector('.deviceAccessContainer').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.deviceAccessContainer').classList.remove('hide');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user, mediaFolders, channels, devices) => {
|
||||
setUserName(user.Name);
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadChannels(user, channels);
|
||||
loadMediaFolders(user, mediaFolders);
|
||||
loadDevices(user, devices);
|
||||
loading.hide();
|
||||
}, [loadChannels, loadDevices, loadMediaFolders]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} });
|
||||
const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promise3 = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||
const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
const onSubmit = (e) => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
|
||||
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
|
||||
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllDevices = element?.current?.querySelector('.chkEnableAllDevices').checked;
|
||||
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkDevice'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.BlockedChannels = null;
|
||||
user.Policy.BlockedMediaFolders = null;
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveComplete = () => {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllDevices').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.deviceAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.channelAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.folderAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.userLibraryAccessForm').addEventListener('submit', onSubmit);
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTabs activeTab='userlibraryaccess'/>
|
||||
<form className='userLibraryAccessForm'>
|
||||
<div className='folderAccessContainer'>
|
||||
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllFolders'
|
||||
title='OptionEnableAccessToAllLibraries'
|
||||
/>
|
||||
<div className='folderAccessListContainer'>
|
||||
<div className='folderAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderLibraries')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
{mediaFoldersItems.map(Item => {
|
||||
return (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LibraryAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='channelAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllChannels'
|
||||
title='OptionEnableAccessToAllChannels'
|
||||
/>
|
||||
<div className='channelAccessListContainer'>
|
||||
<div className='channelAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('Channels')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('ChannelAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='deviceAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderDeviceAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllDevices'
|
||||
title='OptionEnableAccessFromAllDevices'
|
||||
/>
|
||||
<div className='deviceAccessListContainer'>
|
||||
<div className='deviceAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderDevices')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{devicesItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
AppName={Item.AppName}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('DeviceAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserLibraryAccessPage;
|
378
src/components/pages/UserParentalControl.tsx
Normal file
378
src/components/pages/UserParentalControl.tsx
Normal file
@ -0,0 +1,378 @@
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appRouter } from '../appRouter';
|
||||
import AccessScheduleList from '../dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import loading from '../loading/loading';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
type RatingsArr = {
|
||||
Name: string;
|
||||
Value: number;
|
||||
}
|
||||
|
||||
type ItemsArr = {
|
||||
name: string;
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ parentalRatings, setParentalRatings ] = useState([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState([]);
|
||||
const [ accessSchedules, setAccessSchedules ] = useState([]);
|
||||
const [ blockedTags, setBlockedTags ] = useState([]);
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const populateRatings = useCallback((allParentalRatings) => {
|
||||
let rating;
|
||||
const ratings: RatingsArr[] = [];
|
||||
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
rating = allParentalRatings[i];
|
||||
|
||||
if (ratings.length) {
|
||||
const lastRating = ratings[ratings.length - 1];
|
||||
|
||||
if (lastRating.Value === rating.Value) {
|
||||
lastRating.Name += '/' + rating.Name;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ratings.push({
|
||||
Name: rating.Name,
|
||||
Value: rating.Value
|
||||
});
|
||||
}
|
||||
|
||||
setParentalRatings(ratings);
|
||||
}, []);
|
||||
|
||||
const loadUnratedItems = useCallback((user) => {
|
||||
const items = [{
|
||||
name: globalize.translate('Books'),
|
||||
value: 'Book'
|
||||
}, {
|
||||
name: globalize.translate('Channels'),
|
||||
value: 'ChannelContent'
|
||||
}, {
|
||||
name: globalize.translate('LiveTV'),
|
||||
value: 'LiveTvChannel'
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'Movie'
|
||||
}, {
|
||||
name: globalize.translate('Music'),
|
||||
value: 'Music'
|
||||
}, {
|
||||
name: globalize.translate('Trailers'),
|
||||
value: 'Trailer'
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'Series'
|
||||
}];
|
||||
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setUnratedItems(itemsArr);
|
||||
|
||||
const blockUnratedItems = element?.current?.querySelector('.blockUnratedItems');
|
||||
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
||||
}, []);
|
||||
|
||||
const loadBlockedTags = useCallback((tags) => {
|
||||
setBlockedTags(tags);
|
||||
|
||||
const blockedTagsElem = element?.current?.querySelector('.blockedTags');
|
||||
|
||||
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
||||
btnDeleteTag.addEventListener('click', function () {
|
||||
const tag = btnDeleteTag.getAttribute('data-tag');
|
||||
const newTags = tags.filter(function (t) {
|
||||
return t != tag;
|
||||
});
|
||||
loadBlockedTags(newTags);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderAccessSchedule = useCallback((schedules) => {
|
||||
setAccessSchedules(schedules);
|
||||
|
||||
const accessScheduleList = element?.current?.querySelector('.accessScheduleList');
|
||||
|
||||
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
|
||||
btnDelete.addEventListener('click', function () {
|
||||
const index = parseInt(btnDelete.getAttribute('data-index'));
|
||||
schedules.splice(index, 1);
|
||||
const newindex = schedules.filter(function (i) {
|
||||
return i != index;
|
||||
});
|
||||
renderAccessSchedule(newindex);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user, allParentalRatings) => {
|
||||
setUserName(user.Name);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
loadUnratedItems(user);
|
||||
|
||||
loadBlockedTags(user.Policy.BlockedTags);
|
||||
populateRatings(allParentalRatings);
|
||||
let ratingValue = '';
|
||||
|
||||
if (user.Policy.MaxParentalRating) {
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
const rating = allParentalRatings[i];
|
||||
|
||||
if (user.Policy.MaxParentalRating >= rating.Value) {
|
||||
ratingValue = rating.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.current.querySelector('.selectMaxParentalRating').value = ratingValue;
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
element?.current?.querySelector('.accessScheduleSection').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.accessScheduleSection').classList.remove('hide');
|
||||
}
|
||||
renderAccessSchedule(user.Policy.AccessSchedules || []);
|
||||
loading.hide();
|
||||
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
const promise1 = window.ApiClient.getUser(userId);
|
||||
const promise2 = window.ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1]);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
const onSaveComplete = () => {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
};
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Policy.MaxParentalRating = element?.current?.querySelector('.selectMaxParentalRating').value || null;
|
||||
user.Policy.BlockUnratedItems = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkUnratedItem'), function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-itemtype');
|
||||
});
|
||||
user.Policy.AccessSchedules = getSchedulesFromPage();
|
||||
user.Policy.BlockedTags = getBlockedTagsFromPage();
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
};
|
||||
|
||||
const showSchedulePopup = (schedule, index) => {
|
||||
schedule = schedule || {};
|
||||
import('../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
|
||||
accessschedule.show({
|
||||
schedule: schedule
|
||||
}).then(function (updatedSchedule) {
|
||||
const schedules = getSchedulesFromPage();
|
||||
|
||||
if (index == -1) {
|
||||
index = schedules.length;
|
||||
}
|
||||
|
||||
schedules[index] = updatedSchedule;
|
||||
renderAccessSchedule(schedules);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getSchedulesFromPage = () => {
|
||||
return Array.prototype.map.call(element?.current?.querySelectorAll('.liSchedule'), function (elem) {
|
||||
return {
|
||||
DayOfWeek: elem.getAttribute('data-day'),
|
||||
StartHour: elem.getAttribute('data-start'),
|
||||
EndHour: elem.getAttribute('data-end')
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getBlockedTagsFromPage = () => {
|
||||
return Array.prototype.map.call(element?.current?.querySelectorAll('.blockedTag'), function (elem) {
|
||||
return elem.getAttribute('data-tag');
|
||||
});
|
||||
};
|
||||
|
||||
const showBlockedTagPopup = () => {
|
||||
import('../../components/prompt/prompt').then(({default: prompt}) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelTag')
|
||||
}).then(function (value) {
|
||||
const tags = getBlockedTagsFromPage();
|
||||
|
||||
if (tags.indexOf(value) == -1) {
|
||||
tags.push(value);
|
||||
loadBlockedTags(tags);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.btnAddSchedule').addEventListener('click', function () {
|
||||
showSchedulePopup({}, -1);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.btnAddBlockedTag').addEventListener('click', function () {
|
||||
showBlockedTagPopup();
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.userParentalControlForm').addEventListener('submit', onSubmit);
|
||||
}, [loadBlockedTags, loadData, renderAccessSchedule]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTabs activeTab='userparentalcontrol'/>
|
||||
<form className='userParentalControlForm'>
|
||||
<div className='selectContainer'>
|
||||
<SelectMaxParentalRating
|
||||
className= 'selectMaxParentalRating'
|
||||
label= 'LabelMaxParentalRating'
|
||||
parentalRatings={parentalRatings}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('MaxParentalRatingHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='blockUnratedItems'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderBlockItemsWithNoRating')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
{unratedItems.map(Item => {
|
||||
return <CheckBoxListItem
|
||||
key={Item.value}
|
||||
className='chkUnratedItem'
|
||||
ItemType={Item.value}
|
||||
Name={Item.name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div
|
||||
className='detailSectionHeader sectionTitleContainer'
|
||||
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||
>
|
||||
<h2 className='sectionTitle'>
|
||||
{globalize.translate('LabelBlockContentWithTags')}
|
||||
</h2>
|
||||
<SectionTitleButtonElement
|
||||
className='fab btnAddBlockedTag submit'
|
||||
title='Add'
|
||||
icon='add'
|
||||
/>
|
||||
</div>
|
||||
<div className='blockedTags' style={{marginTop: '.5em'}}>
|
||||
{blockedTags.map((tag, index) => {
|
||||
return <BlockedTagList
|
||||
key={index}
|
||||
tag={tag}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div
|
||||
className='sectionTitleContainer'
|
||||
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||
>
|
||||
<h2 className='sectionTitle'>
|
||||
{globalize.translate('HeaderAccessSchedule')}
|
||||
</h2>
|
||||
<SectionTitleButtonElement
|
||||
className='fab btnAddSchedule submit'
|
||||
title='Add'
|
||||
icon='add'
|
||||
/>
|
||||
</div>
|
||||
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
|
||||
<div className='accessScheduleList paperList'>
|
||||
{accessSchedules.map((accessSchedule, index) => {
|
||||
return <AccessScheduleList
|
||||
key={index}
|
||||
index={index}
|
||||
DayOfWeek={accessSchedule.DayOfWeek}
|
||||
StartHour={accessSchedule.StartHour}
|
||||
EndHour={accessSchedule.EndHour}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserParentalControl;
|
@ -3499,7 +3499,7 @@ class PlaybackManager {
|
||||
this.seek(ticks, player);
|
||||
}
|
||||
|
||||
playTrailers(item) {
|
||||
async playTrailers(item) {
|
||||
const player = this._currentPlayer;
|
||||
|
||||
if (player && player.playTrailers) {
|
||||
@ -3508,33 +3508,31 @@ class PlaybackManager {
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
const instance = this;
|
||||
let items;
|
||||
|
||||
if (item.LocalTrailerCount) {
|
||||
return apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(function (result) {
|
||||
return instance.play({
|
||||
items: result
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const remoteTrailers = item.RemoteTrailers || [];
|
||||
items = await apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id);
|
||||
}
|
||||
|
||||
if (!remoteTrailers.length) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.play({
|
||||
items: remoteTrailers.map(function (t) {
|
||||
return {
|
||||
Name: t.Name || (item.Name + ' Trailer'),
|
||||
Url: t.Url,
|
||||
MediaType: 'Video',
|
||||
Type: 'Trailer',
|
||||
ServerId: apiClient.serverId()
|
||||
};
|
||||
})
|
||||
if (!items || !items.length) {
|
||||
items = (item.RemoteTrailers || []).map((t) => {
|
||||
return {
|
||||
Name: t.Name || (item.Name + ' Trailer'),
|
||||
Url: t.Url,
|
||||
MediaType: 'Video',
|
||||
Type: 'Trailer',
|
||||
ServerId: apiClient.serverId()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
return this.play({
|
||||
items
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
getSubtitleUrl(textStream, serverId) {
|
||||
|
@ -20,6 +20,8 @@ import ServerConnections from '../ServerConnections';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import template from './recordingcreator.template.html';
|
||||
|
||||
import PlaceholderImage from './empty.png';
|
||||
|
||||
let currentDialog;
|
||||
let closeAction;
|
||||
let currentRecordingFields;
|
||||
@ -70,7 +72,7 @@ function renderRecording(context, defaultTimer, program, apiClient, refreshRecor
|
||||
const imageContainer = context.querySelector('.recordingDialog-imageContainer');
|
||||
|
||||
if (imgUrl) {
|
||||
imageContainer.innerHTML = '<img src="./empty.png" data-src="' + imgUrl + '" class="recordingDialog-img lazy" />';
|
||||
imageContainer.innerHTML = `<img src="${PlaceholderImage}" data-src="${imgUrl}" class="recordingDialog-img lazy" />`;
|
||||
imageContainer.classList.remove('hide');
|
||||
|
||||
imageLoader.lazyChildren(imageContainer);
|
||||
|
@ -203,6 +203,7 @@
|
||||
.layout-desktop .playlistSectionButton,
|
||||
.layout-tv .playlistSectionButton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.layout-desktop .nowPlayingPlaylist,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import dom from '../../../scripts/dom';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
@ -96,7 +97,7 @@ import confirm from '../../../components/confirm/confirm';
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="${canEdit ? '#!/device.html?id=' + device.Id : '#'}" class="cardContent cardImageContainer">`;
|
||||
deviceHtml += `<a is="emby-linkbutton" href="${canEdit ? '#!/device.html?id=' + device.Id : '#'}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
|
@ -13,13 +13,12 @@
|
||||
<select is="emby-select" id="selectVideoDecoder" label="${LabelHardwareAccelerationType}">
|
||||
<option value="">${None}</option>
|
||||
<option value="amf">AMD AMF</option>
|
||||
<option value="qsv">Intel Quick Sync</option>
|
||||
<option value="mediacodec">MediaCodec Android</option>
|
||||
<option value="omx">OpenMAX OMX</option>
|
||||
<option value="nvenc">Nvidia NVENC</option>
|
||||
<option value="qsv">Intel QuickSync (QSV)</option>
|
||||
<option value="vaapi">Video Acceleration API (VAAPI)</option>
|
||||
<option value="h264_v4l2m2m">Exynos V4L2 MFC</option>
|
||||
<option value="videotoolbox">Video ToolBox</option>
|
||||
<option value="videotoolbox">Apple VideoToolBox</option>
|
||||
<option value="v4l2m2m">Video4Linux2 (V4L2)</option>
|
||||
<option value="omx">OpenMAX OMX</option>
|
||||
</select>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://docs.jellyfin.org/general/administration/hardware-acceleration.html" target="_blank">${LabelHardwareAccelerationTypeHelp}</a>
|
||||
@ -31,57 +30,53 @@
|
||||
<div class="fieldDescription">${LabelVaapiDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer hide fldOpenclDevice">
|
||||
<input is="emby-input" type="text" id="txtOpenclDevice" label="${LabelOpenclDevice}" />
|
||||
<div class="fieldDescription">${LabelOpenclDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="hardwareAccelerationOptions hide">
|
||||
<div class="checkboxListContainer decodingCodecsList">
|
||||
<h3 class="checkboxListLabel">${LabelEnableHardwareDecodingFor}</h3>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="h264" data-types="amf,qsv,nvenc,vaapi,omx,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="h264" data-types="amf,nvenc,qsv,vaapi,videotoolbox,v4l2m2m,omx" />
|
||||
<span>H264</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="hevc" data-types="amf,qsv,nvenc,vaapi,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="hevc" data-types="amf,nvenc,qsv,vaapi,videotoolbox" />
|
||||
<span>HEVC</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg2video" data-types="amf,qsv,nvenc,vaapi,omx,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg2video" data-types="amf,nvenc,qsv,vaapi,videotoolbox" />
|
||||
<span>MPEG2</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="amf,nvenc,omx,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="amf,nvenc,videotoolbox" />
|
||||
<span>MPEG4</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vc1" data-types="amf,qsv,nvenc,vaapi,omx,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vc1" data-types="amf,nvenc,qsv,vaapi,videotoolbox" />
|
||||
<span>VC1</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp8" data-types="qsv,nvenc,vaapi,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp8" data-types="nvenc,qsv,vaapi,videotoolbox" />
|
||||
<span>VP8</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp9" data-types="amf,qsv,nvenc,vaapi,mediacodec,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp9" data-types="amf,nvenc,qsv,vaapi,videotoolbox" />
|
||||
<span>VP9</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="av1" data-types="amf,nvenc,qsv,vaapi" />
|
||||
<span>AV1</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fld10bitHevcVp9HwDecoding">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Hevc" />
|
||||
<span>HEVC 10bit</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Vp9" />
|
||||
<span>VP9 10bit</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Hevc" />
|
||||
<span>${EnableDecodingColorDepth10Hevc}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxListContainer">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Vp9" />
|
||||
<span>${EnableDecodingColorDepth10Vp9}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer hide fldEnhancedNvdec">
|
||||
@ -91,13 +86,34 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer hide fldSysNativeHwDecoder">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSystemNativeHwDecoder" />
|
||||
<span>${PreferSystemNativeHwDecoder}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer">
|
||||
<h3 class="checkboxListLabel">${LabelHardwareEncodingOptions}</h3>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkHardwareEncoding" />
|
||||
<span>${EnableHardwareEncoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fldIntelLp">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIntelLpH264HwEncoder" />
|
||||
<span>${EnableIntelLowPowerH264HwEncoder}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIntelLpHevcHwEncoder" />
|
||||
<span>${EnableIntelLowPowerHevcHwEncoder}</span>
|
||||
</label>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://01.org/linuxgraphics/downloads/firmware" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -177,6 +193,14 @@
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
<option value="15">15</option>
|
||||
<option value="16">16</option>
|
||||
<option value="0">${OptionMax}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${LabelTranscodingThreadCountHelp}</div>
|
||||
|
@ -15,6 +15,9 @@ import alert from '../../components/alert';
|
||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
||||
page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder;
|
||||
page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder;
|
||||
page.querySelector('#chkIntelLpHevcHwEncoder').checked = config.EnableIntelLowPowerHevcHwEncoder;
|
||||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||
@ -28,7 +31,6 @@ import alert from '../../components/alert';
|
||||
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#txtOpenclDevice').value = config.OpenclDevice || '';
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
@ -81,7 +83,6 @@ import alert from '../../components/alert';
|
||||
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
||||
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||
config.OpenclDevice = form.querySelector('#txtOpenclDevice').value;
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
@ -105,6 +106,9 @@ import alert from '../../components/alert';
|
||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked;
|
||||
config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked;
|
||||
config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked;
|
||||
config.EnableIntelLowPowerHevcHwEncoder = form.querySelector('#chkIntelLpHevcHwEncoder').checked;
|
||||
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||
@ -182,32 +186,42 @@ import alert from '../../components/alert';
|
||||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc' || this.value == 'amf') {
|
||||
page.querySelector('.fldOpenclDevice').classList.remove('hide');
|
||||
page.querySelector('#txtOpenclDevice').setAttribute('required', 'required');
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
} else if (this.value == 'vaapi') {
|
||||
page.querySelector('.fldOpenclDevice').classList.add('hide');
|
||||
page.querySelector('#txtOpenclDevice').removeAttribute('required');
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'videotoolbox') {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldOpenclDevice').classList.add('hide');
|
||||
page.querySelector('#txtOpenclDevice').removeAttribute('required');
|
||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldIntelLp').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldIntelLp').classList.add('hide');
|
||||
}
|
||||
|
||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
||||
page.querySelector('.fldVppTonemapping').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldVppTonemapping').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv') {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc') {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
||||
}
|
||||
|
||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'vaapi' || this.value == 'qsv')) {
|
||||
page.querySelector('.fldVppTonemapping').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldVppTonemapping').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||
} else {
|
||||
|
@ -6,6 +6,9 @@
|
||||
<button is="emby-button" type="button" class="fab btnNewRepository submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/plugins/index.html#repositories">
|
||||
${Help}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="repositories"></div>
|
||||
|
@ -1,194 +1,3 @@
|
||||
<div id="editUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle username"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" id="userProfileNavigation" data-mini="true">
|
||||
<a href="#" is="emby-linkbutton" data-role="button" class="ui-btn-active">${Profile}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
|
||||
</div>
|
||||
<p class="lnkEditUserPreferencesContainer">
|
||||
<a class="lnkEditUserPreferences button-link" href="#" is="emby-linkbutton">${ButtonEditOtherUserPreferences}</a>
|
||||
</p>
|
||||
<form class="editUserProfileForm">
|
||||
|
||||
<div class="disabledUserBanner" style="display: none;">
|
||||
<div class="btn btnDarkAccent btnStatic">
|
||||
<div>
|
||||
${HeaderThisUserIsCurrentlyDisabled}
|
||||
</div>
|
||||
<div style="margin-top: 5px;">
|
||||
${MessageReenableUser}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="fldUserName" class="inputContainer">
|
||||
<input is="emby-input" id="txtUserName" required type="text" label="${LabelName}" />
|
||||
</div>
|
||||
|
||||
<div class="selectContainer fldSelectLoginProvider hide">
|
||||
<select class="selectLoginProvider" is="emby-select" label="${LabelAuthProvider}"></select>
|
||||
<div class="fieldDescription">${AuthProviderHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer fldSelectPasswordResetProvider hide">
|
||||
<select class="selectPasswordResetProvider" is="emby-select" label="${LabelPasswordResetProvider}"></select>
|
||||
<div class="fieldDescription">${PasswordResetProviderHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
|
||||
<span>${AllowRemoteAccess}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
|
||||
</div>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIsAdmin" />
|
||||
<span>${OptionAllowUserToManageServer}</span>
|
||||
</label>
|
||||
<div id="featureAccessFields" class="verticalSection">
|
||||
<h2 class="paperListLabel">${HeaderFeatureAccess}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableLiveTvAccess" />
|
||||
<span>${OptionAllowBrowsingLiveTv}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkManageLiveTv" />
|
||||
<span>${OptionAllowManageLiveTv}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="paperListLabel">${HeaderPlayback}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableMediaPlayback" />
|
||||
<span>${OptionAllowMediaPlayback}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAudioPlaybackTranscoding" />
|
||||
<span>${OptionAllowAudioPlaybackTranscoding}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackTranscoding" />
|
||||
<span>${OptionAllowVideoPlaybackTranscoding}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackRemuxing" />
|
||||
<span>${OptionAllowVideoPlaybackRemuxing}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkForceRemoteSourceTranscoding" />
|
||||
<span>${OptionForceRemoteSourceTranscoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fieldDescription">${OptionAllowMediaPlaybackTranscodingHelp}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="verticalSection">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
|
||||
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
|
||||
<div class="fieldDescription">${LabelUserRemoteClientBitrateLimitHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<div class="selectContainer fldSelectSyncPlayAccess">
|
||||
<select class="selectSyncPlayAccess" is="emby-select" id="selectSyncPlayAccess" label="${LabelSyncPlayAccess}">
|
||||
<option value="CreateAndJoinGroups">${LabelSyncPlayAccessCreateAndJoinGroups}</option>
|
||||
<option value="JoinGroups">${LabelSyncPlayAccessJoinGroups}</option>
|
||||
<option value="None">${LabelSyncPlayAccessNone}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${SyncPlayAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="checkboxListLabel" style="margin-bottom:1em;">${HeaderAllowMediaDeletionFrom}</h2>
|
||||
<div class="checkboxList paperList checkboxList-paperList">
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableDeleteAllFolders" />
|
||||
<span>${AllLibraries}</span>
|
||||
</label>
|
||||
<div class="deleteAccess">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="checkboxListLabel">${HeaderRemoteControl}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableRemoteControlOtherUsers" />
|
||||
<span>${OptionAllowRemoteControlOthers}</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteControlSharedDevices" />
|
||||
<span>${OptionAllowRemoteSharedDevices}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fieldDescription">${OptionAllowRemoteSharedDevicesHelp}</div>
|
||||
</div>
|
||||
<h2 class="checkboxListLabel">${Other}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableDownloading" />
|
||||
<span>${OptionAllowContentDownload}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionAllowContentDownloadHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsEnabled">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDisabled" />
|
||||
<span>${OptionDisableUser}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionDisableUserHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsHidden">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIsHidden" />
|
||||
<span>${OptionHideUser}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionHideUserFromLoginHelp}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class=verticalSection>
|
||||
<div class="inputContainer" id="fldLoginAttemptsBeforeLockout">
|
||||
<input is="emby-input" type="number" id="txtLoginAttemptsBeforeLockout" min="-1" step="1" label="${LabelUserLoginAttemptsBeforeLockout}"/>
|
||||
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockout}</div>
|
||||
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockoutHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class=verticalSection>
|
||||
<div class="inputContainer" id="fldMaxActiveSessions">
|
||||
<input is="emby-input" type="number" id="txtMaxActiveSessions" min="0" step="1" label="${LabelUserMaxActiveSessions}"/>
|
||||
<div class="fieldDescription">${OptionMaxActiveSessions}</div>
|
||||
<div class="fieldDescription">${OptionMaxActiveSessionsHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,196 +0,0 @@
|
||||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import Dashboard from '../../../scripts/clientUtils';
|
||||
import toast from '../../../components/toast/toast';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function loadDeleteFolders(page, user, mediaFolders) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
let isChecked;
|
||||
let checkedAttribute;
|
||||
let html = '';
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
for (const folder of channelsResult.Items) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
$('.deleteAccess', page).html(html).trigger('create');
|
||||
$('#chkEnableDeleteAllFolders', page).prop('checked', user.Policy.EnableContentDeletion);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAuthProviders(page, user, providers) {
|
||||
if (providers.length > 1) {
|
||||
page.querySelector('.fldSelectLoginProvider').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSelectLoginProvider').classList.add('hide');
|
||||
}
|
||||
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
page.querySelector('.selectLoginProvider').innerHTML = providers.map(function (provider) {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadPasswordResetProviders(page, user, providers) {
|
||||
if (providers.length > 1) {
|
||||
page.querySelector('.fldSelectPasswordResetProvider').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSelectPasswordResetProvider').classList.add('hide');
|
||||
}
|
||||
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
page.querySelector('.selectPasswordResetProvider').innerHTML = providers.map(function (provider) {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadUser(page, user) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(page, user, providers);
|
||||
});
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(page, user, providers);
|
||||
});
|
||||
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(page, user, folders.Items);
|
||||
});
|
||||
|
||||
if (user.Policy.IsDisabled) {
|
||||
$('.disabledUserBanner', page).show();
|
||||
} else {
|
||||
$('.disabledUserBanner', page).hide();
|
||||
}
|
||||
|
||||
$('#txtUserName', page).prop('disabled', '').removeAttr('disabled');
|
||||
$('#fldConnectInfo', page).show();
|
||||
$('.lnkEditUserPreferences', page).attr('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
libraryMenu.setTitle(user.Name);
|
||||
page.querySelector('.username').innerHTML = user.Name;
|
||||
$('#txtUserName', page).val(user.Name);
|
||||
$('#chkIsAdmin', page).prop('checked', user.Policy.IsAdministrator);
|
||||
$('#chkDisabled', page).prop('checked', user.Policy.IsDisabled);
|
||||
$('#chkIsHidden', page).prop('checked', user.Policy.IsHidden);
|
||||
$('#chkRemoteControlSharedDevices', page).prop('checked', user.Policy.EnableSharedDeviceControl);
|
||||
$('#chkEnableRemoteControlOtherUsers', page).prop('checked', user.Policy.EnableRemoteControlOfOtherUsers);
|
||||
$('#chkEnableDownloading', page).prop('checked', user.Policy.EnableContentDownloading);
|
||||
$('#chkManageLiveTv', page).prop('checked', user.Policy.EnableLiveTvManagement);
|
||||
$('#chkEnableLiveTvAccess', page).prop('checked', user.Policy.EnableLiveTvAccess);
|
||||
$('#chkEnableMediaPlayback', page).prop('checked', user.Policy.EnableMediaPlayback);
|
||||
$('#chkEnableAudioPlaybackTranscoding', page).prop('checked', user.Policy.EnableAudioPlaybackTranscoding);
|
||||
$('#chkEnableVideoPlaybackTranscoding', page).prop('checked', user.Policy.EnableVideoPlaybackTranscoding);
|
||||
$('#chkEnableVideoPlaybackRemuxing', page).prop('checked', user.Policy.EnablePlaybackRemuxing);
|
||||
$('#chkForceRemoteSourceTranscoding', page).prop('checked', user.Policy.ForceRemoteSourceTranscoding);
|
||||
$('#chkRemoteAccess', page).prop('checked', user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess);
|
||||
$('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || '');
|
||||
$('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0');
|
||||
$('#txtMaxActiveSessions', page).val(user.Policy.MaxActiveSessions || '0');
|
||||
if (ApiClient.isMinServerVersion('10.6.0')) {
|
||||
$('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess);
|
||||
}
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
function saveUser(user, page) {
|
||||
user.Name = $('#txtUserName', page).val();
|
||||
user.Policy.IsAdministrator = $('#chkIsAdmin', page).is(':checked');
|
||||
user.Policy.IsHidden = $('#chkIsHidden', page).is(':checked');
|
||||
user.Policy.IsDisabled = $('#chkDisabled', page).is(':checked');
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = $('#chkEnableRemoteControlOtherUsers', page).is(':checked');
|
||||
user.Policy.EnableLiveTvManagement = $('#chkManageLiveTv', page).is(':checked');
|
||||
user.Policy.EnableLiveTvAccess = $('#chkEnableLiveTvAccess', page).is(':checked');
|
||||
user.Policy.EnableSharedDeviceControl = $('#chkRemoteControlSharedDevices', page).is(':checked');
|
||||
user.Policy.EnableMediaPlayback = $('#chkEnableMediaPlayback', page).is(':checked');
|
||||
user.Policy.EnableAudioPlaybackTranscoding = $('#chkEnableAudioPlaybackTranscoding', page).is(':checked');
|
||||
user.Policy.EnableVideoPlaybackTranscoding = $('#chkEnableVideoPlaybackTranscoding', page).is(':checked');
|
||||
user.Policy.EnablePlaybackRemuxing = $('#chkEnableVideoPlaybackRemuxing', page).is(':checked');
|
||||
user.Policy.ForceRemoteSourceTranscoding = $('#chkForceRemoteSourceTranscoding', page).is(':checked');
|
||||
user.Policy.EnableContentDownloading = $('#chkEnableDownloading', page).is(':checked');
|
||||
user.Policy.EnableRemoteAccess = $('#chkRemoteAccess', page).is(':checked');
|
||||
user.Policy.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat($('#txtRemoteClientBitrateLimit', page).val() || '0'));
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt($('#txtLoginAttemptsBeforeLockout', page).val() || '0');
|
||||
user.Policy.MaxActiveSessions = parseInt($('#txtMaxActiveSessions', page).val() || '0');
|
||||
user.Policy.AuthenticationProviderId = page.querySelector('.selectLoginProvider').value;
|
||||
user.Policy.PasswordResetProviderId = page.querySelector('.selectPasswordResetProvider').value;
|
||||
user.Policy.EnableContentDeletion = $('#chkEnableDeleteAllFolders', page).is(':checked');
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : $('.chkFolder', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value;
|
||||
}
|
||||
ApiClient.updateUser(user).then(function () {
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const page = $(this).parents('.page')[0];
|
||||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result, page);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
const userId = getParameterByName('userId');
|
||||
return ApiClient.getUser(userId);
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(page, user);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#editUserPage', function () {
|
||||
$('.editUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
const page = this;
|
||||
$('#chkEnableDeleteAllFolders', this).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.deleteAccess', page).hide();
|
||||
} else {
|
||||
$('.deleteAccess', page).show();
|
||||
}
|
||||
});
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
if (config.EnableRemoteAccess) {
|
||||
page.querySelector('.fldRemoteAccess').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldRemoteAccess').classList.add('hide');
|
||||
}
|
||||
});
|
||||
}).on('pagebeforeshow', '#editUserPage', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
@ -1,68 +1,3 @@
|
||||
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
|
||||
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle username"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);" class="ui-btn-active">${TabAccess}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
|
||||
</div>
|
||||
<form class="userLibraryAccessForm">
|
||||
|
||||
<div class="folderAccessContainer">
|
||||
<h2>${HeaderLibraryAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
|
||||
<span>${OptionEnableAccessToAllLibraries}</span>
|
||||
</label>
|
||||
<div class="folderAccessListContainer">
|
||||
<div class="folderAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${LibraryAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channelAccessContainer" style="display:none;">
|
||||
<h2>${HeaderChannelAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
|
||||
<span>${OptionEnableAccessToAllChannels}</span>
|
||||
</label>
|
||||
<div class="channelAccessListContainer">
|
||||
<div class="channelAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${ChannelAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="deviceAccessContainer hide">
|
||||
<h2>${HeaderDeviceAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllDevices" />
|
||||
<span>${OptionEnableAccessFromAllDevices}</span>
|
||||
</label>
|
||||
<div class="deviceAccessListContainer">
|
||||
<div class="deviceAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${DeviceAccessHelp}</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,186 +0,0 @@
|
||||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import Dashboard from '../../../scripts/clientUtils';
|
||||
import toast from '../../../components/toast/toast';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function triggerChange(select) {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function loadMediaFolders(page, user, mediaFolders) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = mediaFolders.length; i < length; i++) {
|
||||
const folder = mediaFolders[i];
|
||||
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
page.querySelector('.folderAccess').innerHTML = html;
|
||||
const chkEnableAllFolders = page.querySelector('#chkEnableAllFolders');
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}
|
||||
|
||||
function loadChannels(page, user, channels) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = channels.length; i < length; i++) {
|
||||
const folder = channels[i];
|
||||
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.channelAccess', page).show().html(html);
|
||||
|
||||
if (channels.length) {
|
||||
$('.channelAccessContainer', page).show();
|
||||
} else {
|
||||
$('.channelAccessContainer', page).hide();
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = page.querySelector('#chkEnableAllChannels');
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}
|
||||
|
||||
function loadDevices(page, user, devices) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderDevices') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = devices.length; i < length; i++) {
|
||||
const device = devices[i];
|
||||
const checkedAttribute = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1 ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkDevice" data-id="' + device.Id + '" ' + checkedAttribute + '><span>' + device.Name + ' - ' + device.AppName + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.deviceAccess', page).show().html(html);
|
||||
const chkEnableAllDevices = page.querySelector('#chkEnableAllDevices');
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
page.querySelector('.deviceAccessContainer').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.deviceAccessContainer').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function loadUser(page, user, loggedInUser, mediaFolders, channels, devices) {
|
||||
page.querySelector('.username').innerHTML = user.Name;
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadChannels(page, user, channels);
|
||||
loadMediaFolders(page, user, mediaFolders);
|
||||
loadDevices(page, user, devices);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSaveComplete() {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
function saveUser(user, page) {
|
||||
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
|
||||
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : $('.chkFolder', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
|
||||
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : $('.chkChannel', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllDevices = $('#chkEnableAllDevices', page).is(':checked');
|
||||
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : $('.chkDevice', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.BlockedChannels = null;
|
||||
user.Policy.BlockedMediaFolders = null;
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const page = $(this).parents('.page');
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result, page);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#userLibraryAccessPage', function () {
|
||||
const page = this;
|
||||
$('#chkEnableAllDevices', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.deviceAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.deviceAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
$('#chkEnableAllChannels', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.channelAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.channelAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
page.querySelector('#chkEnableAllFolders').addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
page.querySelector('.folderAccessListContainer').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.folderAccessListContainer').classList.remove('hide');
|
||||
}
|
||||
});
|
||||
$('.userLibraryAccessForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#userLibraryAccessPage', function () {
|
||||
const page = this;
|
||||
loading.show();
|
||||
let promise1;
|
||||
const userId = getParameterByName('userId');
|
||||
|
||||
if (userId) {
|
||||
promise1 = ApiClient.getUser(userId);
|
||||
} else {
|
||||
const deferred = $.Deferred();
|
||||
deferred.resolveWith(null, [{
|
||||
Configuration: {}
|
||||
}]);
|
||||
promise1 = deferred.promise();
|
||||
}
|
||||
|
||||
const promise2 = Dashboard.getCurrentUser();
|
||||
const promise4 = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promise5 = ApiClient.getJSON(ApiClient.getUrl('Channels'));
|
||||
const promise6 = ApiClient.getJSON(ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise4, promise5, promise6]).then(function (responses) {
|
||||
loadUser(page, responses[0], responses[1], responses[2].Items, responses[3].Items, responses[4].Items);
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
@ -1,60 +1,3 @@
|
||||
<div id="userParentalControlPage" data-role="page" class="page type-interior">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle username"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);" class="ui-btn-active">${TabParentalControl}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
|
||||
</div>
|
||||
|
||||
<form class="userParentalControlForm">
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectMaxParentalRating" label="${LabelMaxParentalRating}"></select>
|
||||
<div class="fieldDescription">${MaxParentalRatingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="blockUnratedItems"></div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="verticalSection" style="margin-bottom:2em;">
|
||||
<div class="detailSectionHeader sectionTitleContainer">
|
||||
<h2 class="sectionTitle">${LabelBlockContentWithTags}</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddBlockedTag submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="blockedTags" style="margin-top:.5em;"></div>
|
||||
</div>
|
||||
|
||||
<div class="accessScheduleSection verticalSection" style="margin-bottom:2em;">
|
||||
<div class="sectionTitleContainer">
|
||||
<h2 class="sectionTitle">${HeaderAccessSchedule}</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddSchedule submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p>${HeaderAccessScheduleHelp}</p>
|
||||
<div class="accessScheduleList paperList"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,274 +0,0 @@
|
||||
import 'jquery';
|
||||
import datetime from '../../../scripts/datetime';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import '../../../components/listview/listview.scss';
|
||||
import '../../../elements/emby-button/paper-icon-button-light';
|
||||
import toast from '../../../components/toast/toast';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function populateRatings(allParentalRatings, page) {
|
||||
let html = '';
|
||||
html += "<option value=''></option>";
|
||||
let rating;
|
||||
const ratings = [];
|
||||
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
if (rating = allParentalRatings[i], ratings.length) {
|
||||
const lastRating = ratings[ratings.length - 1];
|
||||
|
||||
if (lastRating.Value === rating.Value) {
|
||||
lastRating.Name += '/' + rating.Name;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ratings.push({
|
||||
Name: rating.Name,
|
||||
Value: rating.Value
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0, length = ratings.length; i < length; i++) {
|
||||
rating = ratings[i];
|
||||
html += "<option value='" + rating.Value + "'>" + rating.Name + '</option>';
|
||||
}
|
||||
|
||||
$('#selectMaxParentalRating', page).html(html);
|
||||
}
|
||||
|
||||
function loadUnratedItems(page, user) {
|
||||
const items = [{
|
||||
name: globalize.translate('Books'),
|
||||
value: 'Book'
|
||||
}, {
|
||||
name: globalize.translate('Channels'),
|
||||
value: 'ChannelContent'
|
||||
}, {
|
||||
name: globalize.translate('LiveTV'),
|
||||
value: 'LiveTvChannel'
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'Movie'
|
||||
}, {
|
||||
name: globalize.translate('Music'),
|
||||
value: 'Music'
|
||||
}, {
|
||||
name: globalize.translate('Trailers'),
|
||||
value: 'Trailer'
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'Series'
|
||||
}];
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderBlockItemsWithNoRating') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
const checkedAttribute = user.Policy.BlockUnratedItems.indexOf(item.value) != -1 ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkUnratedItem" data-itemtype="' + item.value + '" type="checkbox"' + checkedAttribute + '><span>' + item.name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.blockUnratedItems', page).html(html).trigger('create');
|
||||
}
|
||||
|
||||
function loadUser(page, user, allParentalRatings) {
|
||||
page.querySelector('.username').innerHTML = user.Name;
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadUnratedItems(page, user);
|
||||
loadBlockedTags(page, user.Policy.BlockedTags);
|
||||
populateRatings(allParentalRatings, page);
|
||||
let ratingValue = '';
|
||||
|
||||
if (user.Policy.MaxParentalRating) {
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
const rating = allParentalRatings[i];
|
||||
|
||||
if (user.Policy.MaxParentalRating >= rating.Value) {
|
||||
ratingValue = rating.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#selectMaxParentalRating', page).val(ratingValue);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
$('.accessScheduleSection', page).hide();
|
||||
} else {
|
||||
$('.accessScheduleSection', page).show();
|
||||
}
|
||||
|
||||
renderAccessSchedule(page, user.Policy.AccessSchedules || []);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function loadBlockedTags(page, tags) {
|
||||
let html = tags.map(function (h) {
|
||||
let li = '<div class="listItem">';
|
||||
li += '<div class="listItemBody">';
|
||||
li += '<h3 class="listItemBodyText">';
|
||||
li += h;
|
||||
li += '</h3>';
|
||||
li += '</div>';
|
||||
li += '<button type="button" is="paper-icon-button-light" class="blockedTag btnDeleteTag listItemButton" data-tag="' + h + '"><span class="material-icons delete"></span></button>';
|
||||
return li += '</div>';
|
||||
}).join('');
|
||||
|
||||
if (html) {
|
||||
html = '<div class="paperList">' + html + '</div>';
|
||||
}
|
||||
|
||||
const elem = $('.blockedTags', page).html(html).trigger('create');
|
||||
$('.btnDeleteTag', elem).on('click', function () {
|
||||
const tag = this.getAttribute('data-tag');
|
||||
const newTags = tags.filter(function (t) {
|
||||
return t != tag;
|
||||
});
|
||||
loadBlockedTags(page, newTags);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAccessSchedule(page, schedules, index) {
|
||||
schedules.splice(index, 1);
|
||||
renderAccessSchedule(page, schedules);
|
||||
}
|
||||
|
||||
function renderAccessSchedule(page, schedules) {
|
||||
let html = '';
|
||||
let index = 0;
|
||||
html += schedules.map(function (a) {
|
||||
let itemHtml = '';
|
||||
itemHtml += '<div class="liSchedule listItem" data-day="' + a.DayOfWeek + '" data-start="' + a.StartHour + '" data-end="' + a.EndHour + '">';
|
||||
itemHtml += '<div class="listItemBody two-line">';
|
||||
itemHtml += '<h3 class="listItemBodyText">';
|
||||
itemHtml += globalize.translate('Option' + a.DayOfWeek);
|
||||
itemHtml += '</h3>';
|
||||
itemHtml += '<div class="listItemBodyText secondary">' + getDisplayTime(a.StartHour) + ' - ' + getDisplayTime(a.EndHour) + '</div>';
|
||||
itemHtml += '</div>';
|
||||
itemHtml += '<button type="button" is="paper-icon-button-light" class="btnDelete listItemButton" data-index="' + index + '"><span class="material-icons delete"></span></button>';
|
||||
itemHtml += '</div>';
|
||||
index++;
|
||||
return itemHtml;
|
||||
}).join('');
|
||||
const accessScheduleList = page.querySelector('.accessScheduleList');
|
||||
accessScheduleList.innerHTML = html;
|
||||
$('.btnDelete', accessScheduleList).on('click', function () {
|
||||
deleteAccessSchedule(page, schedules, parseInt(this.getAttribute('data-index')));
|
||||
});
|
||||
}
|
||||
|
||||
function onSaveComplete() {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
function saveUser(user, page) {
|
||||
user.Policy.MaxParentalRating = $('#selectMaxParentalRating', page).val() || null;
|
||||
user.Policy.BlockUnratedItems = $('.chkUnratedItem', page).get().filter(function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-itemtype');
|
||||
});
|
||||
user.Policy.AccessSchedules = getSchedulesFromPage(page);
|
||||
user.Policy.BlockedTags = getBlockedTagsFromPage(page);
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
|
||||
if (pct) {
|
||||
minutes = parseInt(60 * pct);
|
||||
}
|
||||
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
function showSchedulePopup(page, schedule, index) {
|
||||
schedule = schedule || {};
|
||||
import('../../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
|
||||
accessschedule.show({
|
||||
schedule: schedule
|
||||
}).then(function (updatedSchedule) {
|
||||
const schedules = getSchedulesFromPage(page);
|
||||
|
||||
if (index == -1) {
|
||||
index = schedules.length;
|
||||
}
|
||||
|
||||
schedules[index] = updatedSchedule;
|
||||
renderAccessSchedule(page, schedules);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSchedulesFromPage(page) {
|
||||
return $('.liSchedule', page).map(function () {
|
||||
return {
|
||||
DayOfWeek: this.getAttribute('data-day'),
|
||||
StartHour: this.getAttribute('data-start'),
|
||||
EndHour: this.getAttribute('data-end')
|
||||
};
|
||||
}).get();
|
||||
}
|
||||
|
||||
function getBlockedTagsFromPage(page) {
|
||||
return $('.blockedTag', page).map(function () {
|
||||
return this.getAttribute('data-tag');
|
||||
}).get();
|
||||
}
|
||||
|
||||
function showBlockedTagPopup(page) {
|
||||
import('../../../components/prompt/prompt').then(({default: prompt}) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelTag')
|
||||
}).then(function (value) {
|
||||
const tags = getBlockedTagsFromPage(page);
|
||||
|
||||
if (tags.indexOf(value) == -1) {
|
||||
tags.push(value);
|
||||
loadBlockedTags(page, tags);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.UserParentalControlPage = {
|
||||
onSubmit: function () {
|
||||
const page = $(this).parents('.page');
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result, page);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$(document).on('pageinit', '#userParentalControlPage', function () {
|
||||
const page = this;
|
||||
$('.btnAddSchedule', page).on('click', function () {
|
||||
showSchedulePopup(page, {}, -1);
|
||||
});
|
||||
$('.btnAddBlockedTag', page).on('click', function () {
|
||||
showBlockedTagPopup(page);
|
||||
});
|
||||
$('.userParentalControlForm').off('submit', UserParentalControlPage.onSubmit).on('submit', UserParentalControlPage.onSubmit);
|
||||
}).on('pageshow', '#userParentalControlPage', function () {
|
||||
const page = this;
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
const promise1 = ApiClient.getUser(userId);
|
||||
const promise2 = ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadUser(page, responses[0], responses[1]);
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
@ -33,6 +33,12 @@ import ServerConnections from '../../components/ServerConnections';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import { download } from '../../scripts/fileDownloader';
|
||||
|
||||
function autoFocus(container) {
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(container);
|
||||
});
|
||||
}
|
||||
|
||||
function getPromise(apiClient, params) {
|
||||
const id = params.id;
|
||||
|
||||
@ -368,12 +374,12 @@ function reloadPlayButtons(page, item) {
|
||||
hideAll(page, 'btnShuffle');
|
||||
}
|
||||
|
||||
const btnResume = page.querySelector('.mainDetailButtons .btnResume');
|
||||
const btnPlay = page.querySelector('.mainDetailButtons .btnPlay');
|
||||
if (layoutManager.tv && !btnResume.classList.contains('hide')) {
|
||||
btnResume.classList.add('fab');
|
||||
} else if (layoutManager.tv && btnResume.classList.contains('hide')) {
|
||||
btnPlay.classList.add('fab');
|
||||
if (layoutManager.tv) {
|
||||
const btnResume = page.querySelector('.mainDetailButtons .btnResume');
|
||||
const btnPlay = page.querySelector('.mainDetailButtons .btnPlay');
|
||||
const resumeHidden = btnResume.classList.contains('hide');
|
||||
btnResume.classList.toggle('raised', !resumeHidden);
|
||||
btnPlay.classList.toggle('raised', resumeHidden);
|
||||
}
|
||||
|
||||
return canPlay;
|
||||
@ -634,12 +640,6 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||
setInitialCollapsibleState(page, item, apiClient, params.context, user);
|
||||
const canPlay = reloadPlayButtons(page, item);
|
||||
|
||||
if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) {
|
||||
hideAll(page, 'btnPlayTrailer', true);
|
||||
} else {
|
||||
hideAll(page, 'btnPlayTrailer');
|
||||
}
|
||||
|
||||
setTrailerButtonVisibility(page, item);
|
||||
|
||||
if (item.Type !== 'Program' || canPlay) {
|
||||
@ -727,9 +727,7 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||
hideAll(page, 'btnDownload', true);
|
||||
}
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
autoFocus(page);
|
||||
}
|
||||
|
||||
function logoImageUrl(item, apiClient, options) {
|
||||
@ -921,7 +919,7 @@ function setInitialCollapsibleState(page, item, apiClient, context, user) {
|
||||
|
||||
renderScenes(page, item);
|
||||
|
||||
if (item.SpecialFeatureCount && item.SpecialFeatureCount != 0 && item.Type != 'Series') {
|
||||
if (item.SpecialFeatureCount > 0) {
|
||||
page.querySelector('#specialsCollapsible').classList.remove('hide');
|
||||
renderSpecials(page, item, user);
|
||||
} else {
|
||||
@ -1756,9 +1754,7 @@ function renderCollectionItems(page, parentItem, types, items) {
|
||||
|
||||
// HACK: Call autoFocuser again because btnPlay may be hidden, but focused by reloadFromItem
|
||||
// FIXME: Sometimes focus does not move until all (?) sections are loaded
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
autoFocus(page);
|
||||
}
|
||||
|
||||
function renderCollectionItemType(page, parentItem, type, items) {
|
||||
@ -2060,6 +2056,7 @@ export default function (view, params) {
|
||||
currentItem.UserData = userData;
|
||||
reloadPlayButtons(view, currentItem);
|
||||
refreshImage(view, currentItem);
|
||||
autoFocus(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
</div>
|
||||
<div class="flex-grow padded-left padded-right padded-bottom-page pageContainerTopPadding">
|
||||
<div class="flex align-items-center focuscontainer-x itemsViewSettingsContainer padded-top padded-bottom flex-wrap-wrap">
|
||||
<div class="paging"></div>
|
||||
<button is="emby-button" class="btnPlay button-flat hide listTextButton-autohide">
|
||||
<span>${HeaderPlayAll}</span>
|
||||
</button>
|
||||
@ -50,5 +51,8 @@
|
||||
</div>
|
||||
<div is="emby-itemscontainer" class="vertical-wrap itemsContainer centered">
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
||||
<div class="paging"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import globalize from '../scripts/globalize';
|
||||
import listView from '../components/listview/listview';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import * as userSettings from '../scripts/settings/userSettings';
|
||||
import focusManager from '../components/focusManager';
|
||||
import cardBuilder from '../components/cardbuilder/cardBuilder';
|
||||
import loading from '../components/loading/loading';
|
||||
import AlphaNumericShortcuts from '../scripts/alphanumericshortcuts';
|
||||
import libraryBrowser from '../scripts/libraryBrowser';
|
||||
import { playbackManager } from '../components/playback/playbackmanager';
|
||||
import AlphaPicker from '../components/alphaPicker/alphaPicker';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
@ -15,12 +15,12 @@ import { appRouter } from '../components/appRouter';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function getInitialLiveTvQuery(instance, params) {
|
||||
function getInitialLiveTvQuery(instance, params, startIndex = 0, limit = 300) {
|
||||
const query = {
|
||||
UserId: ServerConnections.getApiClient(params.serverId).getCurrentUserId(),
|
||||
StartIndex: 0,
|
||||
StartIndex: startIndex,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio',
|
||||
Limit: 300
|
||||
Limit: limit
|
||||
};
|
||||
|
||||
if (params.type === 'Recordings') {
|
||||
@ -165,7 +165,14 @@ import { appRouter } from '../components/appRouter';
|
||||
instance.setFilterStatus(hasFilters);
|
||||
|
||||
if (instance.alphaPicker) {
|
||||
query.NameStartsWith = instance.alphaPicker.value();
|
||||
const newValue = instance.alphaPicker.value();
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
@ -237,15 +244,15 @@ import { appRouter } from '../components/appRouter';
|
||||
|
||||
instance.queryRecursive = false;
|
||||
if (params.type === 'Recordings') {
|
||||
return apiClient.getLiveTvRecordings(getInitialLiveTvQuery(instance, params));
|
||||
return apiClient.getLiveTvRecordings(getInitialLiveTvQuery(instance, params, startIndex, limit));
|
||||
}
|
||||
|
||||
if (params.type === 'Programs') {
|
||||
if (params.IsAiring === 'true') {
|
||||
return apiClient.getLiveTvRecommendedPrograms(getInitialLiveTvQuery(instance, params));
|
||||
return apiClient.getLiveTvRecommendedPrograms(getInitialLiveTvQuery(instance, params, startIndex, limit));
|
||||
}
|
||||
|
||||
return apiClient.getLiveTvPrograms(getInitialLiveTvQuery(instance, params));
|
||||
return apiClient.getLiveTvPrograms(getInitialLiveTvQuery(instance, params, startIndex, limit));
|
||||
}
|
||||
|
||||
if (params.type === 'nextup') {
|
||||
@ -423,14 +430,75 @@ import { appRouter } from '../components/appRouter';
|
||||
|
||||
class ItemsView {
|
||||
constructor(view, params) {
|
||||
const query = {
|
||||
StartIndex: 0,
|
||||
Limit: undefined
|
||||
};
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query['Limit'] = userSettings.libraryPageSize();
|
||||
}
|
||||
|
||||
let isLoading = false;
|
||||
|
||||
function onNextPageClick() {
|
||||
if (!isLoading && query.Limit > 0) {
|
||||
query.StartIndex += query.Limit;
|
||||
self.itemsContainer.refreshItems().then(() => {
|
||||
window.scrollTo(0, 0);
|
||||
autoFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onPreviousPageClick() {
|
||||
if (!isLoading && query.Limit > 0) {
|
||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
||||
self.itemsContainer.refreshItems().then(() => {
|
||||
window.scrollTo(0, 0);
|
||||
autoFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaging(startIndex, totalRecordCount, limit) {
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex,
|
||||
limit,
|
||||
totalRecordCount,
|
||||
showLimit: false,
|
||||
updatePageSizeSetting: false,
|
||||
addLayoutButton: false,
|
||||
sortButton: false,
|
||||
filterButton: false
|
||||
});
|
||||
|
||||
for (const elem of view.querySelectorAll('.paging')) {
|
||||
elem.innerHTML = pagingHtml;
|
||||
}
|
||||
|
||||
for (const elem of view.querySelectorAll('.btnNextPage')) {
|
||||
elem.addEventListener('click', onNextPageClick);
|
||||
}
|
||||
|
||||
for (const elem of view.querySelectorAll('.btnPreviousPage')) {
|
||||
elem.addEventListener('click', onPreviousPageClick);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchData() {
|
||||
return getItems(self, params, self.currentItem).then(function (result) {
|
||||
isLoading = true;
|
||||
|
||||
return getItems(self, params, self.currentItem, null, query.StartIndex, query.Limit).then(function (result) {
|
||||
if (self.totalItemCount == null) {
|
||||
self.totalItemCount = result.Items ? result.Items.length : result.length;
|
||||
}
|
||||
|
||||
updateAlphaPickerState(self, self.totalItemCount);
|
||||
updatePaging(result.StartIndex, result.TotalRecordCount, query.Limit);
|
||||
return result;
|
||||
}).finally(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -559,15 +627,13 @@ class ItemsView {
|
||||
|
||||
self.alphaPicker = new AlphaPicker({
|
||||
element: alphaPickerElement,
|
||||
itemsContainer: layoutManager.tv ? self.itemsContainer : null,
|
||||
itemClass: 'card',
|
||||
valueChangeEvent: layoutManager.tv ? null : 'click'
|
||||
valueChangeEvent: 'click'
|
||||
});
|
||||
self.alphaPicker.on('alphavaluechanged', onAlphaPickerValueChanged);
|
||||
}
|
||||
|
||||
function onAlphaPickerValueChanged() {
|
||||
self.alphaPicker.value();
|
||||
query.StartIndex = 0;
|
||||
self.itemsContainer.refreshItems();
|
||||
}
|
||||
|
||||
@ -670,7 +736,7 @@ class ItemsView {
|
||||
autoplay: true
|
||||
});
|
||||
} else {
|
||||
getItems(self, self.params, currentItem, null, null, 300).then(function (result) {
|
||||
getItems(self, self.params, currentItem, null, 0, 300).then(function (result) {
|
||||
playbackManager.play({
|
||||
items: result.Items,
|
||||
autoplay: true
|
||||
@ -687,7 +753,7 @@ class ItemsView {
|
||||
items: [currentItem]
|
||||
});
|
||||
} else {
|
||||
getItems(self, self.params, currentItem, null, null, 300).then(function (result) {
|
||||
getItems(self, self.params, currentItem, null, 0, 300).then(function (result) {
|
||||
playbackManager.queue({
|
||||
items: result.Items
|
||||
});
|
||||
@ -701,7 +767,7 @@ class ItemsView {
|
||||
if (currentItem && !self.hasFilters) {
|
||||
playbackManager.shuffle(currentItem);
|
||||
} else {
|
||||
getItems(self, self.params, currentItem, 'Random', null, 300).then(function (result) {
|
||||
getItems(self, self.params, currentItem, 'Random', 0, 300).then(function (result) {
|
||||
playbackManager.play({
|
||||
items: result.Items,
|
||||
autoplay: true
|
||||
@ -710,6 +776,12 @@ class ItemsView {
|
||||
}
|
||||
}
|
||||
|
||||
function autoFocus() {
|
||||
import('../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(view);
|
||||
});
|
||||
}
|
||||
|
||||
const self = this;
|
||||
self.params = params;
|
||||
this.itemsContainer = view.querySelector('.itemsContainer');
|
||||
|
@ -64,6 +64,9 @@
|
||||
<button is="paper-icon-button-light" class="btnFilter sectionTitleButton" title="${Filter}"><span class="material-icons filter_list"></span></button>
|
||||
</div>
|
||||
<div is="emby-itemscontainer" id="items" class="itemsContainer vertical-wrap padded-left padded-right"></div>
|
||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
||||
<div class="paging"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pageTabContent" id="recordingsTab" data-index="3">
|
||||
<div id="latestRecordings" class="verticalSection hide">
|
||||
|
@ -50,7 +50,9 @@ export default function (view, params, tabContent) {
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex += query.Limit;
|
||||
}
|
||||
reloadItems(context);
|
||||
reloadItems(context).then(() => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function onPreviousPageClick() {
|
||||
@ -61,18 +63,24 @@ export default function (view, params, tabContent) {
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
||||
}
|
||||
reloadItems(context);
|
||||
reloadItems(context).then(() => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
const query = getQuery();
|
||||
context.querySelector('.paging').innerHTML = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
totalRecordCount: result.TotalRecordCount,
|
||||
showLimit: false,
|
||||
updatePageSizeSetting: false,
|
||||
filterButton: false
|
||||
});
|
||||
|
||||
for (const elem of context.querySelectorAll('.paging')) {
|
||||
elem.innerHTML = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
totalRecordCount: result.TotalRecordCount,
|
||||
showLimit: false,
|
||||
updatePageSizeSetting: false,
|
||||
filterButton: false
|
||||
});
|
||||
}
|
||||
|
||||
const html = getChannelsHtml(result.Items);
|
||||
const elem = context.querySelector('#items');
|
||||
elem.innerHTML = html;
|
||||
@ -110,13 +118,13 @@ export default function (view, params, tabContent) {
|
||||
const query = getQuery();
|
||||
const apiClient = ApiClient;
|
||||
query.UserId = apiClient.getCurrentUserId();
|
||||
apiClient.getLiveTvChannels(query).then(function (result) {
|
||||
return apiClient.getLiveTvChannels(query).then(function (result) {
|
||||
renderChannels(context, result);
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(view);
|
||||
autoFocuser.autoFocus(context);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ function renderActiveRecordings(context, promise) {
|
||||
defaultShape: getBackdropShape(),
|
||||
showParentTitle: false,
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
showTitle: true,
|
||||
showAirTime: true,
|
||||
showAirEndTime: true,
|
||||
showChannelName: true,
|
||||
|
@ -2,6 +2,7 @@ import 'jquery';
|
||||
import globalize from '../scripts/globalize';
|
||||
import taskButton from '../scripts/taskbutton';
|
||||
import dom from '../scripts/dom';
|
||||
import cardBuilder from '../components/cardbuilder/cardBuilder';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import loading from '../components/loading/loading';
|
||||
import browser from '../scripts/browser';
|
||||
@ -37,7 +38,7 @@ function getDeviceHtml(device) {
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="' + padderClass + '"></div>';
|
||||
html += '<div class="cardContent searchImage">';
|
||||
html += '<div class="cardImageContainer coveredImage"><span class="cardImageIcon material-icons dvr"></span></div>';
|
||||
html += `<div class="cardImageContainer coveredImage ${cardBuilder.getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr"></span></div>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
|
@ -17,7 +17,7 @@ import '../../elements/emby-button/emby-button';
|
||||
if (!pageData) {
|
||||
pageData = data[key] = {
|
||||
query: {
|
||||
SortBy: 'Random',
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: true,
|
||||
@ -70,7 +70,7 @@ import '../../elements/emby-button/emby-button';
|
||||
|
||||
const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary';
|
||||
const query = {
|
||||
SortBy: 'SortName',
|
||||
SortBy: 'Random',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: true,
|
||||
|
@ -23,24 +23,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
itemsContainer.innerHTML = '';
|
||||
};
|
||||
|
||||
const updateFilterControls = () => {
|
||||
if (this.alphaPicker) {
|
||||
this.alphaPicker.value(query.NameStartsWith);
|
||||
if (query.SortBy.indexOf('SortName') === 0) {
|
||||
this.alphaPicker.visible(true);
|
||||
} else {
|
||||
this.alphaPicker.visible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function fetchData() {
|
||||
isLoading = true;
|
||||
loading.show();
|
||||
return ApiClient.getItems(ApiClient.getCurrentUserId(), query);
|
||||
}
|
||||
|
||||
function afterRefresh(result) {
|
||||
const afterRefresh = (result) => {
|
||||
function onNextPageClick() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
@ -64,7 +53,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
updateFilterControls();
|
||||
this.alphaPicker?.updateControls(query);
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
@ -94,7 +83,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(tabContent);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getItemsHtml = (items) => {
|
||||
let html;
|
||||
@ -173,7 +162,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
if (alphaPickerElement) {
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
||||
const newValue = e.detail.value;
|
||||
query.NameStartsWith = newValue;
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
itemsContainer.refreshItems();
|
||||
});
|
||||
@ -301,9 +296,9 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
onViewStyleChange();
|
||||
};
|
||||
|
||||
this.renderTab = function () {
|
||||
this.renderTab = () => {
|
||||
itemsContainer.refreshItems();
|
||||
updateFilterControls();
|
||||
this.alphaPicker?.updateControls(query);
|
||||
};
|
||||
|
||||
this.destroy = function () {
|
||||
|
@ -81,7 +81,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
updateFilterControls(tabContent);
|
||||
this.alphaPicker?.updateControls(query);
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
@ -183,11 +183,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterControls = (tabContent) => {
|
||||
const query = getQuery(tabContent);
|
||||
this.alphaPicker.value(query.NameStartsWith);
|
||||
};
|
||||
|
||||
const data = {};
|
||||
let isLoading = false;
|
||||
|
||||
@ -216,7 +211,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
||||
const newValue = e.detail.value;
|
||||
const query = getQuery(tabContent);
|
||||
query.NameStartsWith = newValue;
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
reloadItems();
|
||||
});
|
||||
@ -268,9 +269,9 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
initPage(tabContent);
|
||||
|
||||
this.renderTab = function () {
|
||||
this.renderTab = () => {
|
||||
reloadItems();
|
||||
updateFilterControls(tabContent);
|
||||
this.alphaPicker?.updateControls(getQuery(tabContent));
|
||||
};
|
||||
|
||||
this.destroy = function () {};
|
||||
|
@ -112,7 +112,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
updateFilterControls();
|
||||
this.alphaPicker?.updateControls(query);
|
||||
let html;
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
@ -185,20 +185,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterControls = () => {
|
||||
const query = getQuery();
|
||||
|
||||
if (this.alphaPicker) {
|
||||
this.alphaPicker.value(query.NameStartsWith);
|
||||
|
||||
if (query.SortBy.indexOf('SortName') === 0) {
|
||||
this.alphaPicker.visible(true);
|
||||
} else {
|
||||
this.alphaPicker.visible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let savedQueryKey;
|
||||
let pageData;
|
||||
let isLoading = false;
|
||||
@ -230,7 +216,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
||||
const newValue = e.detail.value;
|
||||
const query = getQuery();
|
||||
query.NameStartsWith = newValue;
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
reloadItems();
|
||||
});
|
||||
@ -302,9 +294,9 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
initPage(tabContent);
|
||||
onViewStyleChange();
|
||||
|
||||
this.renderTab = function () {
|
||||
this.renderTab = () => {
|
||||
reloadItems();
|
||||
updateFilterControls();
|
||||
this.alphaPicker?.updateControls(getQuery());
|
||||
};
|
||||
|
||||
this.destroy = function () {};
|
||||
|
@ -99,7 +99,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
updateFilterControls(page);
|
||||
this.alphaPicker?.updateControls(query);
|
||||
let html;
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
@ -167,11 +167,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterControls = (tabContent) => {
|
||||
const query = getQuery(tabContent);
|
||||
this.alphaPicker.value(query.NameStartsWith);
|
||||
};
|
||||
|
||||
const data = {};
|
||||
let isLoading = false;
|
||||
|
||||
@ -201,7 +196,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
||||
const newValue = e.detail.value;
|
||||
const query = getQuery(tabContent);
|
||||
query.NameStartsWith = newValue;
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
});
|
||||
@ -234,9 +235,9 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
initPage(tabContent);
|
||||
onViewStyleChange();
|
||||
|
||||
this.renderTab = function () {
|
||||
this.renderTab = () => {
|
||||
reloadItems(tabContent);
|
||||
updateFilterControls(tabContent);
|
||||
this.alphaPicker?.updateControls(getQuery(tabContent));
|
||||
};
|
||||
|
||||
this.destroy = function () {};
|
||||
|
@ -27,7 +27,6 @@ import cardBuilder from '../../../components/cardbuilder/cardBuilder';
|
||||
|
||||
onLoginSuccessful(user.Id, result.AccessToken, apiClient);
|
||||
}, function (response) {
|
||||
page.querySelector('#txtManualName').value = '';
|
||||
page.querySelector('#txtManualPassword').value = '';
|
||||
loading.hide();
|
||||
|
||||
|
@ -17,7 +17,7 @@ import '../../elements/emby-button/emby-button';
|
||||
if (!pageData) {
|
||||
pageData = data[key] = {
|
||||
query: {
|
||||
SortBy: 'Random',
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Series',
|
||||
Recursive: true,
|
||||
@ -70,7 +70,7 @@ import '../../elements/emby-button/emby-button';
|
||||
|
||||
const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary';
|
||||
const query = {
|
||||
SortBy: 'SortName',
|
||||
SortBy: 'Random',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Series',
|
||||
Recursive: true,
|
||||
|
@ -97,7 +97,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
updateFilterControls(page);
|
||||
this.alphaPicker?.updateControls(query);
|
||||
let html;
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
@ -196,20 +196,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterControls = (tabContent) => {
|
||||
const query = getQuery(tabContent);
|
||||
|
||||
if (this.alphaPicker) {
|
||||
this.alphaPicker.value(query.NameStartsWith);
|
||||
|
||||
if (query.SortBy.indexOf('SortName') === 0) {
|
||||
this.alphaPicker.visible(true);
|
||||
} else {
|
||||
this.alphaPicker.visible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const data = {};
|
||||
let isLoading = false;
|
||||
|
||||
@ -239,7 +225,13 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
||||
const newValue = e.detail.value;
|
||||
const query = getQuery(tabContent);
|
||||
query.NameStartsWith = newValue;
|
||||
if (newValue === '#') {
|
||||
query.NameLessThan = 'A';
|
||||
delete query.NameStartsWith;
|
||||
} else {
|
||||
query.NameStartsWith = newValue;
|
||||
delete query.NameLessThan;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
});
|
||||
@ -301,9 +293,9 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
initPage(tabContent);
|
||||
onViewStyleChange();
|
||||
|
||||
this.renderTab = function () {
|
||||
this.renderTab = () => {
|
||||
reloadItems(tabContent);
|
||||
updateFilterControls(tabContent);
|
||||
this.alphaPicker?.updateControls(getQuery(tabContent));
|
||||
};
|
||||
|
||||
this.destroy = function () {};
|
||||
|
@ -112,6 +112,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="exitApp listItem-border">
|
||||
<div class="listItem">
|
||||
<span class="material-icons listItemIcon listItemIcon-transparent close"></span>
|
||||
<div class="listItemBody">
|
||||
<div class="listItemBodyText">${ButtonExitApp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,6 +17,10 @@ export default function (view, params) {
|
||||
window.NativeShell.openClientSettings();
|
||||
});
|
||||
|
||||
view.querySelector('.exitApp').addEventListener('click', function () {
|
||||
appHost.exit();
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function () {
|
||||
// this page can also be used by admins to change user preferences from the user edit page
|
||||
const userId = params.userId || Dashboard.getCurrentUserId();
|
||||
@ -33,6 +37,9 @@ export default function (view, params) {
|
||||
const supportsClientSettings = appHost.supports('clientsettings');
|
||||
page.querySelector('.clientSettings').classList.toggle('hide', !supportsClientSettings);
|
||||
|
||||
const supportsExitMenu = appHost.supports('exitmenu');
|
||||
page.querySelector('.exitApp').classList.toggle('hide', !supportsExitMenu);
|
||||
|
||||
const supportsMultiServer = appHost.supports('multiserver');
|
||||
page.querySelector('.selectServer').classList.toggle('hide', !supportsMultiServer);
|
||||
|
||||
|
@ -108,6 +108,11 @@
|
||||
width: 12em;
|
||||
}
|
||||
|
||||
.checkboxList > .sectioncheckbox > .emby-checkbox-label {
|
||||
display: flex;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.checkboxList-paperList {
|
||||
padding: 1em !important;
|
||||
}
|
||||
|
@ -138,15 +138,15 @@ import 'webcomponents.js/webcomponents-lite';
|
||||
label.innerHTML = this.getAttribute('label') || '';
|
||||
label.classList.add('selectLabel');
|
||||
label.htmlFor = this.id;
|
||||
this.parentNode.insertBefore(label, this);
|
||||
this.parentNode?.insertBefore(label, this);
|
||||
|
||||
if (this.classList.contains('emby-select-withcolor')) {
|
||||
this.parentNode.insertAdjacentHTML('beforeend', '<div class="selectArrowContainer"><div style="visibility:hidden;display:none;">0</div><span class="selectArrow material-icons keyboard_arrow_down"></span></div>');
|
||||
this.parentNode?.insertAdjacentHTML('beforeend', '<div class="selectArrowContainer"><div style="visibility:hidden;display:none;">0</div><span class="selectArrow material-icons keyboard_arrow_down"></span></div>');
|
||||
}
|
||||
};
|
||||
|
||||
EmbySelectPrototype.setLabel = function (text) {
|
||||
const label = this.parentNode.querySelector('label');
|
||||
const label = this.parentNode?.querySelector('label');
|
||||
|
||||
label.innerHTML = text;
|
||||
};
|
||||
|
@ -44,6 +44,12 @@ export class BookPlayer {
|
||||
stop() {
|
||||
this.unbindEvents();
|
||||
|
||||
const stopInfo = {
|
||||
src: this.item
|
||||
};
|
||||
|
||||
Events.trigger(this, 'stopped', [stopInfo]);
|
||||
|
||||
const elem = this.mediaElement;
|
||||
const tocElement = this.tocElement;
|
||||
const rendition = this.rendition;
|
||||
@ -67,6 +73,10 @@ export class BookPlayer {
|
||||
this.cancellationToken = true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
currentItem() {
|
||||
return this.item;
|
||||
}
|
||||
@ -149,8 +159,8 @@ export class BookPlayer {
|
||||
bindMediaElementEvents() {
|
||||
const elem = this.mediaElement;
|
||||
|
||||
elem.addEventListener('close', this.onDialogClosed, {once: true});
|
||||
elem.querySelector('#btnBookplayerExit').addEventListener('click', this.onDialogClosed, {once: true});
|
||||
elem.addEventListener('close', this.onDialogClosed, { once: true });
|
||||
elem.querySelector('#btnBookplayerExit').addEventListener('click', this.onDialogClosed, { once: true });
|
||||
elem.querySelector('#btnBookplayerToc').addEventListener('click', this.openTableOfContents);
|
||||
elem.querySelector('#btnBookplayerFullscreen').addEventListener('click', this.toggleFullscreen);
|
||||
elem.querySelector('#btnBookplayerPrev')?.addEventListener('click', this.previous);
|
||||
@ -250,6 +260,7 @@ export class BookPlayer {
|
||||
this.streamInfo = {
|
||||
started: true,
|
||||
ended: false,
|
||||
item: this.item,
|
||||
mediaSource: {
|
||||
Id: item.Id
|
||||
}
|
||||
@ -263,9 +274,9 @@ export class BookPlayer {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
import('epubjs').then(({default: epubjs}) => {
|
||||
import('epubjs').then(({ default: epubjs }) => {
|
||||
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
|
||||
const book = epubjs(downloadHref, {openAs: 'epub'});
|
||||
const book = epubjs(downloadHref, { openAs: 'epub' });
|
||||
|
||||
// We need to calculate the height of the window beforehand because using 100% is not accurate when the dialog is opening.
|
||||
// In addition we don't render to the full height so that we have space for the top buttons.
|
||||
@ -284,7 +295,7 @@ export class BookPlayer {
|
||||
|
||||
return rendition.display().then(() => {
|
||||
const epubElem = document.querySelector('.epub-container');
|
||||
epubElem.style.display = 'none';
|
||||
epubElem.style.opacity = '0';
|
||||
|
||||
this.bindEvents();
|
||||
|
||||
@ -298,10 +309,10 @@ export class BookPlayer {
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
epubElem.style.display = 'block';
|
||||
epubElem.style.opacity = '';
|
||||
rendition.on('relocated', (locations) => {
|
||||
this.progress = book.locations.percentageFromCfi(locations.start.cfi);
|
||||
Events.trigger(this, 'timeupdate');
|
||||
Events.trigger(this, 'pause');
|
||||
});
|
||||
|
||||
loading.hide();
|
||||
|
@ -1048,12 +1048,16 @@ function tryRemoveElement(elem) {
|
||||
* @private
|
||||
*/
|
||||
renderSsaAss(videoElement, track, item) {
|
||||
const supportedFonts = ['application/x-truetype-font', 'font/otf', 'font/ttf', 'font/woff', 'font/woff2'];
|
||||
const avaliableFonts = [];
|
||||
const attachments = this._currentPlayOptions.mediaSource.MediaAttachments || [];
|
||||
const apiClient = ServerConnections.getApiClient(item);
|
||||
attachments.map(function (i) {
|
||||
// embedded font url
|
||||
return avaliableFonts.push(apiClient.getUrl(i.DeliveryUrl));
|
||||
attachments.forEach(i => {
|
||||
// we only require font files and ignore embedded media attachments like covers as there are cases where ffmpeg fails to extract those
|
||||
if (supportedFonts.includes(i.MimeType)) {
|
||||
// embedded font url
|
||||
avaliableFonts.push(apiClient.getUrl(i.DeliveryUrl));
|
||||
}
|
||||
});
|
||||
const fallbackFontList = apiClient.getUrl('/FallbackFont/Fonts', {
|
||||
api_key: apiClient.accessToken()
|
||||
@ -1865,9 +1869,10 @@ function tryRemoveElement(elem) {
|
||||
};
|
||||
categories.push(videoCategory);
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = mediaElement.getBoundingClientRect ? mediaElement.getBoundingClientRect() : {};
|
||||
let height = parseInt(rect.height);
|
||||
let width = parseInt(rect.width);
|
||||
let height = Math.round(rect.height * devicePixelRatio);
|
||||
let width = Math.round(rect.width * devicePixelRatio);
|
||||
|
||||
// Don't show player dimensions on smart TVs because the app UI could be lower resolution than the video and this causes users to think there is a problem
|
||||
if (width && height && !browser.tv) {
|
||||
|
@ -36,6 +36,12 @@ export class PdfPlayer {
|
||||
stop() {
|
||||
this.unbindEvents();
|
||||
|
||||
const stopInfo = {
|
||||
src: this.item
|
||||
};
|
||||
|
||||
Events.trigger(this, 'stopped', [stopInfo]);
|
||||
|
||||
const elem = this.mediaElement;
|
||||
if (elem) {
|
||||
dialogHelper.close(elem);
|
||||
@ -49,6 +55,10 @@ export class PdfPlayer {
|
||||
this.cancellationToken = true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
currentItem() {
|
||||
return this.item;
|
||||
}
|
||||
@ -114,8 +124,8 @@ export class PdfPlayer {
|
||||
bindMediaElementEvents() {
|
||||
const elem = this.mediaElement;
|
||||
|
||||
elem.addEventListener('close', this.onDialogClosed, {once: true});
|
||||
elem.querySelector('.btnExit').addEventListener('click', this.onDialogClosed, {once: true});
|
||||
elem.addEventListener('close', this.onDialogClosed, { once: true });
|
||||
elem.querySelector('.btnExit').addEventListener('click', this.onDialogClosed, { once: true });
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
@ -181,6 +191,7 @@ export class PdfPlayer {
|
||||
this.streamInfo = {
|
||||
started: true,
|
||||
ended: false,
|
||||
item: this.item,
|
||||
mediaSource: {
|
||||
Id: item.Id
|
||||
}
|
||||
@ -218,12 +229,16 @@ export class PdfPlayer {
|
||||
if (this.progress === this.duration() - 1) return;
|
||||
this.loadPage(this.progress + 2);
|
||||
this.progress = this.progress + 1;
|
||||
|
||||
Events.trigger(this, 'pause');
|
||||
}
|
||||
|
||||
previous() {
|
||||
if (this.progress === 0) return;
|
||||
this.loadPage(this.progress);
|
||||
this.progress = this.progress - 1;
|
||||
|
||||
Events.trigger(this, 'pause');
|
||||
}
|
||||
|
||||
replaceCanvas(canvas) {
|
||||
@ -265,8 +280,6 @@ export class PdfPlayer {
|
||||
|
||||
renderPage(canvas, number) {
|
||||
this.book.getPage(number).then(page => {
|
||||
Events.trigger(this, 'timeupdate');
|
||||
|
||||
const original = page.getViewport({ scale: 1 });
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
|
@ -2,6 +2,11 @@ function isTv() {
|
||||
// This is going to be really difficult to get right
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
// The OculusBrowsers userAgent also has the samsungbrowser defined but is not a tv.
|
||||
if (userAgent.indexOf('oculusbrowser') !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userAgent.indexOf('tv') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ import browser from './browser';
|
||||
function testCanPlayAv1(videoTestElement) {
|
||||
if (browser.tizenVersion >= 5.5) {
|
||||
return true;
|
||||
} else if (browser.web0sVersion >= 5 && window.outerHeight >= 2160) {
|
||||
} else if (browser.web0sVersion >= 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -294,12 +294,55 @@ import browser from './browser';
|
||||
(browser.tizen && isTizenFhd ? 20000000 : null)));
|
||||
}
|
||||
|
||||
function getSpeakerCount() {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext || false; /* eslint-disable-line compat/compat */
|
||||
|
||||
if (AudioContext) {
|
||||
const audioCtx = new AudioContext();
|
||||
|
||||
return audioCtx.destination.maxChannelCount;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getPhysicalAudioChannels(options) {
|
||||
const allowedAudioChannels = parseInt(userSettings.allowedAudioChannels(), 10);
|
||||
|
||||
if (allowedAudioChannels > 0) {
|
||||
return allowedAudioChannels;
|
||||
}
|
||||
|
||||
if (options.audioChannels) {
|
||||
return options.audioChannels;
|
||||
}
|
||||
|
||||
const isSurroundSoundSupportedBrowser = browser.safari || browser.chrome || browser.edgeChromium || browser.firefox || browser.tv || browser.ps4 || browser.xboxOne;
|
||||
const speakerCount = getSpeakerCount();
|
||||
|
||||
if (speakerCount > 2) {
|
||||
if (isSurroundSoundSupportedBrowser) {
|
||||
return speakerCount;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (speakerCount > 0) {
|
||||
return speakerCount;
|
||||
}
|
||||
|
||||
if (isSurroundSoundSupportedBrowser) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
export default function (options) {
|
||||
options = options || {};
|
||||
|
||||
const isSurroundSoundSupportedBrowser = browser.safari || browser.chrome || browser.edgeChromium || browser.firefox;
|
||||
const allowedAudioChannels = parseInt(userSettings.allowedAudioChannels() || '-1');
|
||||
const physicalAudioChannels = (allowedAudioChannels > 0 ? allowedAudioChannels : null) || options.audioChannels || (isSurroundSoundSupportedBrowser || browser.tv || browser.ps4 || browser.xboxOne ? 6 : 2);
|
||||
const physicalAudioChannels = getPhysicalAudioChannels(options);
|
||||
|
||||
const bitrateSetting = getMaxBitrate();
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
case 'AndroidTV':
|
||||
case 'Android TV':
|
||||
return baseUrl + 'android.svg';
|
||||
case 'Jellyfin Mobile (iOS)':
|
||||
return baseUrl + 'apple.svg';
|
||||
case 'Jellyfin Web':
|
||||
switch (device.Name || device.DeviceName) {
|
||||
case 'Opera':
|
||||
|
@ -302,6 +302,11 @@ import Headroom from 'headroom.js';
|
||||
|
||||
html += '<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnSettings" data-itemid="settings" href="#"><span class="material-icons navMenuOptionIcon settings"></span><span class="navMenuOptionText">' + globalize.translate('Settings') + '</span></a>';
|
||||
html += '<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnLogout" data-itemid="logout" href="#"><span class="material-icons navMenuOptionIcon exit_to_app"></span><span class="navMenuOptionText">' + globalize.translate('ButtonSignOut') + '</span></a>';
|
||||
|
||||
if (appHost.supports('exitmenu')) {
|
||||
html += '<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder exitApp" data-itemid="exitapp" href="#"><span class="material-icons navMenuOptionIcon close"></span><span class="navMenuOptionText">' + globalize.translate('ButtonExitApp') + '</span></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
@ -318,6 +323,11 @@ import Headroom from 'headroom.js';
|
||||
btnSettings.addEventListener('click', onSettingsClick);
|
||||
}
|
||||
|
||||
const btnExit = navDrawerScrollContainer.querySelector('.exitApp');
|
||||
if (btnExit) {
|
||||
btnExit.addEventListener('click', onExitAppClick);
|
||||
}
|
||||
|
||||
const btnLogout = navDrawerScrollContainer.querySelector('.btnLogout');
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', onLogoutClick);
|
||||
@ -706,6 +716,10 @@ import Headroom from 'headroom.js';
|
||||
Dashboard.navigate('mypreferencesmenu.html');
|
||||
}
|
||||
|
||||
function onExitAppClick() {
|
||||
appHost.exit();
|
||||
}
|
||||
|
||||
function onLogoutClick() {
|
||||
Dashboard.logout();
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ function getTimersHtml(timers, options) {
|
||||
html += cardBuilder.getCardsHtml({
|
||||
items: group.items,
|
||||
shape: getBackdropShape(),
|
||||
showTitle: true,
|
||||
showParentTitleOrTitle: true,
|
||||
showAirTime: true,
|
||||
showAirEndTime: true,
|
||||
|
@ -440,7 +440,7 @@ import { appRouter } from '../components/appRouter';
|
||||
path: 'dashboard/users/useredit.html',
|
||||
autoFocus: false,
|
||||
roles: 'admin',
|
||||
controller: 'dashboard/users/useredit'
|
||||
pageComponent: 'UserEditPage'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
@ -448,7 +448,7 @@ import { appRouter } from '../components/appRouter';
|
||||
path: 'dashboard/users/userlibraryaccess.html',
|
||||
autoFocus: false,
|
||||
roles: 'admin',
|
||||
controller: 'dashboard/users/userlibraryaccess'
|
||||
pageComponent: 'UserLibraryAccessPage'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
@ -464,7 +464,7 @@ import { appRouter } from '../components/appRouter';
|
||||
path: 'dashboard/users/userparentalcontrol.html',
|
||||
autoFocus: false,
|
||||
roles: 'admin',
|
||||
controller: 'dashboard/users/userparentalcontrol'
|
||||
pageComponent: 'UserParentalControl'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
|
@ -137,11 +137,11 @@ function loadPlugins() {
|
||||
return getPlugins().then(function (list) {
|
||||
if (!appHost.supports('remotecontrol')) {
|
||||
// Disable remote player plugins if not supported
|
||||
list.splice(list.indexOf('sessionPlayer'), 1);
|
||||
list.splice(list.indexOf('chromecastPlayer'), 1);
|
||||
list = list.filter(plugin => !plugin.startsWith('sessionPlayer')
|
||||
&& !plugin.startsWith('chromecastPlayer'));
|
||||
} else if (!browser.chrome && !browser.edgeChromium && !browser.opera) {
|
||||
// Disable chromecast player in unsupported browsers
|
||||
list.splice(list.indexOf('chromecastPlayer'), 1);
|
||||
list = list.filter(plugin => !plugin.startsWith('chromecastPlayer'));
|
||||
}
|
||||
|
||||
// add any native plugins
|
||||
|
@ -362,7 +362,7 @@
|
||||
"HeaderResponseProfile": "Terugvoer Profiel",
|
||||
"HeaderRemoveMediaLocation": "Verwyder Media Lokasie",
|
||||
"HeaderRemoveMediaFolder": "Verwyder Media Gidslêer",
|
||||
"HeaderRemoteControl": "Afstandbeheer",
|
||||
"HeaderRemoteControl": "Afstandbeheer:",
|
||||
"HeaderRemoteAccessSettings": "Eksterne Toegang Verstellings",
|
||||
"HeaderRecordingPostProcessing": "Opname Naverwerking",
|
||||
"HeaderRecordingOptions": "Opname Opsies",
|
||||
@ -373,7 +373,7 @@
|
||||
"HeaderPluginInstallation": "Inprop-toepassing Installasie",
|
||||
"HeaderPleaseSignIn": "Skakel asseblief in",
|
||||
"HeaderPlaybackError": "Terugspeel Fout",
|
||||
"HeaderPlayback": "Media Terugspeel",
|
||||
"HeaderPlayback": "Media Terugspeel:",
|
||||
"HeaderPlayOn": "Speel Aan",
|
||||
"HeaderPlayAll": "Speel Alles",
|
||||
"HeaderPinCodeReset": "Herstel PIN Kode",
|
||||
@ -427,7 +427,7 @@
|
||||
"HeaderForKids": "Vir Kinders",
|
||||
"HeaderFetcherSettings": "Soeker Verstellings",
|
||||
"HeaderFetchImages": "Soek Beelde:",
|
||||
"HeaderFeatureAccess": "Funksie Toegang",
|
||||
"HeaderFeatureAccess": "Funksie Toegang:",
|
||||
"HeaderExternalIds": "Eksterne IDs:",
|
||||
"HeaderError": "Fout",
|
||||
"HeaderEnabledFieldsHelp": "Ontkies 'n veld om dit te sluit en te verhoed dat die veld se data verander kan word.",
|
||||
@ -476,7 +476,7 @@
|
||||
"HeaderApiKeysHelp": "Eksterne toepassings word vereis om 'n API sleutel te hê om te kommunikeer met die bediener. Sleutels word utigereik deur in te sluit in die normale verbruikers rekening of deur self 'n sleutel toe te staan aan die toepassing.",
|
||||
"HeaderApiKeys": "API Sleutels",
|
||||
"HeaderApiKey": "API Sleutel",
|
||||
"HeaderAllowMediaDeletionFrom": "Laat Media Verwydering Toe Van",
|
||||
"HeaderAllowMediaDeletionFrom": "Laat Media Verwydering Toe Van:",
|
||||
"HeaderAlert": "Waarskuwing",
|
||||
"HeaderAdmin": "Administrasie",
|
||||
"HeaderAdditionalParts": "Addisionele Dele",
|
||||
@ -724,7 +724,7 @@
|
||||
"HeaderUploadImage": "Laai Foto",
|
||||
"HeaderUpcomingOnTV": "Opkomend op TV",
|
||||
"HeaderTypeText": "Voer teks",
|
||||
"HeaderTypeImageFetchers": "Beeld gaanhaalers ({0})",
|
||||
"HeaderTypeImageFetchers": "Beeld gaanhaalers ({0}):",
|
||||
"HeaderTuners": "Ontvangers",
|
||||
"HeaderTunerDevices": "Ontvanger Toestele",
|
||||
"HeaderTranscodingProfileHelp": "Las by transkodering profiele om aan te dui watter formate gebruik moet word as transkodering nodig is.",
|
||||
@ -793,5 +793,15 @@
|
||||
"OptionBluray": "BD",
|
||||
"TypeOptionPluralSeries": "TV Programme",
|
||||
"LiveTV": "Lewendige TV",
|
||||
"OptionCriticRating": "Filkritiek Gradering"
|
||||
"OptionCriticRating": "Filkritiek Gradering",
|
||||
"HeaderSyncPlayTimeSyncSettings": "Tydsinkronisering",
|
||||
"HeaderSyncPlayPlaybackSettings": "Afspeel",
|
||||
"HeaderSyncPlaySettings": "SyncPlay Konfigurasie",
|
||||
"ErrorPlayerNotFound": "Geen speler vir die gevraagde media gevind nie.",
|
||||
"Engineer": "Klankingenieur",
|
||||
"Cursive": "Kursief",
|
||||
"Console": "Konsole",
|
||||
"Conductor": "Kondukteur",
|
||||
"Casual": "Informeel",
|
||||
"Arranger": "Organiseerder"
|
||||
}
|
||||
|
@ -91,7 +91,7 @@
|
||||
"Fullscreen": "الشاشة كاملة",
|
||||
"GuideProviderLogin": "تسجيل الدخول",
|
||||
"GuideProviderSelectListings": "إختر المبوبات",
|
||||
"H264CrfHelp": "معامل المعدل الثابت CRF هو الجودة الافتراضية لإعدادات مشفر x264 و x265. بإمكانك إعطاء قيمة تتراوح بين 0 و 51، وكلما قلت القيمة فسينتج عن ذلك جودة أفضل (على حساب حجم تخزين أعلى). القيم المعقولة تتراوح بين 18 و 28. الافتراضي ل x264 هو 23، ول x265 هو 28, لذا فبإمكانك استخدام هذه القيمة كنقطة بداية.",
|
||||
"H264CrfHelp": "معامل المعدل الثابت CRF هو الجودة الافتراضية لإعدادات مشفر x264 و x265. بإمكانك إعطاء قيمة تتراوح بين 0 و 51، وكلما قلة القيمة فسينتج عن ذلك جودة أفضل (على حساب حجم تخزين أعلى). القيم المعقولة تتراوح بين 18 و 28. الافتراضي ل x264 هو 23، ول x265 هو 28, لذا فبإمكانك استخدام هذه القيمة كنقطة بداية.",
|
||||
"EncoderPresetHelp": "اختر قيمة أعلى لتحسين الصورة والأداء وقيمة أقل لتحسين الجودة.",
|
||||
"HardwareAccelerationWarning": "تمكين التسريع بعتاد الحاسوب قد يتسبب في عدم استقرار بعض أنواع الأنظمة. تأكد من أن نظام التشغيل الخاص بك محدث إلى آخر نسخة وأن سواقات الفيديو محدثة أيضاً. إذا واجهت أية صعوبات في تسغيل الفيديو بعد تمكين هذه الخاصية، فعليك إرجاع الإعداد إلى وضعية بلا None.",
|
||||
"HeaderAccessSchedule": "جدول الدخولات",
|
||||
@ -101,9 +101,9 @@
|
||||
"HeaderActivity": "الأنشطة",
|
||||
"HeaderAddUpdateImage": "إضافة/تحديث صورة",
|
||||
"HeaderAdditionalParts": "أدوار إضافية",
|
||||
"HeaderAdmin": "المدير",
|
||||
"HeaderAdmin": "الادارة",
|
||||
"HeaderAlert": "تنبيه",
|
||||
"HeaderAllowMediaDeletionFrom": "السماح بحذف الوسائط من قبل",
|
||||
"HeaderAllowMediaDeletionFrom": "السماح بحذف الوسائط من قبل:",
|
||||
"HeaderApiKey": "مفتاح API",
|
||||
"HeaderApiKeys": "مفاتيح API",
|
||||
"HeaderApiKeysHelp": "التطبيقات الخارجية تحتاج أن تمتلك مفتاح API لكي تتصل بالخادم. هذه المفاتيح تُصدر عن طريق تسجيل الدخول بمستخدم عادي، أو عن طريق منح التطبيق مفتاحاً أصدر يدوياً.",
|
||||
@ -113,14 +113,14 @@
|
||||
"HeaderCastAndCrew": "الممثلين وطاقم العمل",
|
||||
"HeaderChannelAccess": "صلاحيات القنوات",
|
||||
"HeaderCodecProfile": "عريضة الكودك",
|
||||
"HeaderCodecProfileHelp": "عرائض الكودك تشير إلى محدودية جهاز ما عند تشغيل وسيطة مشفر بكودك معيّن. إن كان هناك أي محدودية مذكورة فستحال الوسيطة إلى التشغير البيني، حتى لو كانت الصيغة مضبوطة للعمل بتلقائية.",
|
||||
"HeaderCodecProfileHelp": "تشير ملفات تعريف برنامج الترميز إلى قيود الجهاز عند تشغيل برامج ترميز معينة. إذا تم تطبيق قيود ، فسيتم تحويل ترميز الوسائط ، حتى إذا تم تكوين برنامج الترميز للتشغيل المباشر.",
|
||||
"HeaderConfirmPluginInstallation": "أكد عملية تثبيت الملحق",
|
||||
"HeaderConfirmProfileDeletion": "أكّد حذف العريضة",
|
||||
"HeaderConfirmRevokeApiKey": "أرفض مفتاح API",
|
||||
"HeaderConnectToServer": "اتصل إلى الخادم",
|
||||
"HeaderConnectionFailure": "فشل في الاتصال",
|
||||
"HeaderContainerProfile": "عريضة الحاوية",
|
||||
"HeaderContainerProfileHelp": "عرائض الحاويات تشير إلى محدوديات جهاز ما عند تشغيل صيغ معينة. إن كان هناك أي محدودية مذكورة فستحال الوسيطة إلى التشغير البيني، حتى لو كانت الصيغة مضبوطة للعمل بتلقائية.",
|
||||
"HeaderContainerProfileHelp": "تشير ملفات تعريف الحاوية إلى قيود الجهاز عند تشغيل تنسيقات معينة. إذا تم تطبيق قيود ، فسيتم تحويل ترميز الوسائط ، حتى إذا تم تكوين التنسيق للتشغيل المباشر.",
|
||||
"HeaderContinueWatching": "استمر بالمشاهدة",
|
||||
"HeaderCustomDlnaProfiles": "الحسابات المخصوصة",
|
||||
"HeaderDateIssued": "تاريخ الإصدار",
|
||||
@ -132,11 +132,11 @@
|
||||
"HeaderDeveloperInfo": "معلومات المطور",
|
||||
"HeaderDeviceAccess": "الدخول على جهاز",
|
||||
"HeaderDevices": "الأجهزة",
|
||||
"HeaderDirectPlayProfile": "عريضة التشغيل المباشر",
|
||||
"HeaderDirectPlayProfileHelp": "أضف مباشرةً عريضة تشغيل للإشارة لأي صيغة يتمكن الجهاز من التعامل معه بتلقائية.",
|
||||
"HeaderDirectPlayProfile": "ملف تعريف التشغيل المباشر",
|
||||
"HeaderDirectPlayProfileHelp": "أضف ملفات تعريف التشغيل المباشر للإشارة إلى التنسيقات التي يمكن للجهاز التعامل معها محليًا.",
|
||||
"HeaderEasyPinCode": "الرمز الشخصي البسيط",
|
||||
"HeaderError": "حدث خطأ",
|
||||
"HeaderFeatureAccess": "صلاحية الخاصية",
|
||||
"HeaderFeatureAccess": "صلاحية الخاصية:",
|
||||
"HeaderFetchImages": "إطهار الصور:",
|
||||
"HeaderForKids": "للأطفال",
|
||||
"HeaderFrequentlyPlayed": "تم تشغيله مراراً",
|
||||
@ -168,9 +168,9 @@
|
||||
"HeaderPassword": "كلمة السر",
|
||||
"HeaderPasswordReset": "إعادة تهيئة كلمة السر",
|
||||
"HeaderPaths": "مسارات",
|
||||
"HeaderPinCodeReset": "اعد تهيئة الرمز الشخصي",
|
||||
"HeaderPinCodeReset": "إعادة تهيئة الرمز الشخصي البسيط",
|
||||
"HeaderPlayAll": "تشغيل الكل",
|
||||
"HeaderPlayback": "تشغيل الوسائط",
|
||||
"HeaderPlayback": "تشغيل الوسائط:",
|
||||
"HeaderPleaseSignIn": "الرجاء تسجيل الدخول",
|
||||
"HeaderPluginInstallation": "تثبيت الملحفات",
|
||||
"HeaderPreferredMetadataLanguage": "اللغة المفضلة لواصفات البيانات",
|
||||
@ -178,7 +178,7 @@
|
||||
"HeaderProfileServerSettingsHelp": "هذه القيم ستتحكم في كيفية تقديم شكل الخادم في للعملاء.",
|
||||
"HeaderRecentlyPlayed": "تم تشغيله مؤخراً",
|
||||
"HeaderRecordingPostProcessing": "تطبيق ما-بعد-المعالجة للتسجيل",
|
||||
"HeaderRemoteControl": "التحكم عن بعد",
|
||||
"HeaderRemoteControl": "التحكم عن بعد:",
|
||||
"HeaderRemoveMediaFolder": "إحذف مجلد الوسائط",
|
||||
"HeaderRemoveMediaLocation": "إحذف مكان الوسائط",
|
||||
"HeaderResponseProfile": "عريضة الرد",
|
||||
@ -213,7 +213,7 @@
|
||||
"HeaderTranscodingProfileHelp": "أضف عرائض التشفير البيني للإشارة لأي صيغة يتعيّن استخدامها عندما توجد حاجة للتشفير البيني.",
|
||||
"HeaderTunerDevices": "أجهزة التوليف",
|
||||
"HeaderTuners": "المولفات",
|
||||
"HeaderTypeImageFetchers": "جالبات الصور ({0})",
|
||||
"HeaderTypeImageFetchers": "جالبات الصور ({0}):",
|
||||
"HeaderTypeText": "أدخل النص",
|
||||
"HeaderUpcomingOnTV": "البرامج القادمة على التلفاز",
|
||||
"HeaderUploadImage": "رفع الصور",
|
||||
@ -233,7 +233,7 @@
|
||||
"LabelAirDays": "أيام البث:",
|
||||
"LabelAirTime": "وقت البث:",
|
||||
"LabelAlbum": "الألبوم:",
|
||||
"LabelAlbumArtHelp": "PN المستخدمة في رسومات الألبوم، داخل سمة dlna:profileID في upnp:albumArtURI. بعض الأجهزة تحتاج قيمة محددة، مهما كان حجم الصورة.",
|
||||
"LabelAlbumArtHelp": "يتم استخدام PN لصورة الألبوم ، داخل السمة \"dlna: profileID\" على \"upnp: AlbumArtURI\". تتطلب بعض الأجهزة قيمة معينة ، بغض النظر عن حجم الصورة.",
|
||||
"LabelAlbumArtMaxHeight": "الارتفاع الأقصى لرسومات الألبوم:",
|
||||
"LabelAlbumArtMaxWidth": "العرض الأقصى لرسوم الألبوم:",
|
||||
"LabelAlbumArtPN": "رسومات الألبوم PN:",
|
||||
@ -259,12 +259,12 @@
|
||||
"LabelCurrentPassword": "كلمة السر الحالية:",
|
||||
"LabelCustomCertificatePath": "مسار شهادة SSL المخصص:",
|
||||
"LabelCustomCertificatePathHelp": "مسار ملف PKCS # 12 يحتوي على شهادة ومفتاح خاص لتمكين دعم TLS على مجال مخصص.",
|
||||
"LabelCustomCss": "تنيسق CSS مخصص:",
|
||||
"LabelCustomCssHelp": "طبق تنسيقك css المخصص لواجهة الويب.",
|
||||
"LabelCustomCss": "كود CSS المخصص:",
|
||||
"LabelCustomCssHelp": "قم بتطبيق كود CSS المخصص الخاص بك من أجل السمات / العلامة التجارية على واجهة الويب.",
|
||||
"LabelCustomDeviceDisplayNameHelp": "أذكر اسم عرض مخصوص أو أتركه فارغاً لاستخدام الاسم المبلغ من الجهاز.",
|
||||
"LabelDateAddedBehavior": "كيف يتصرف المحتوى الجديد نحو \"تاريخ الإضافة\" الخاص به:",
|
||||
"LabelDateAddedBehaviorHelp": "إذا اخذت واصفات البيانات قيمة، فإنها سوف تستخدم قبل أن تستخدم أي من هذه الخيارات.",
|
||||
"LabelDay": "اليوم:",
|
||||
"LabelDay": "يوم الأسبوع:",
|
||||
"LabelDeathDate": "تاريخ الوفاة:",
|
||||
"LabelDefaultUser": "المستخدم الافتراضي:",
|
||||
"LabelDefaultUserHelp": "يحدد مكتبة المستخدم التي يجب عرضها على الأجهزة المتصلة. يمكن تجاوز هذا لكل جهاز باستخدام ملفات التعريف.",
|
||||
@ -284,10 +284,10 @@
|
||||
"LabelEnableBlastAliveMessages": "بث رسائل قيد التشغيل",
|
||||
"LabelEnableBlastAliveMessagesHelp": "فعل هذه الخاصية إذا كان الخادم لا يكتشف بكفاءة من قبل أجهزة UPnP الأخرى على شبكتك.",
|
||||
"LabelEnableDlnaClientDiscoveryInterval": "فترات استكشاف العملاء:",
|
||||
"LabelEnableDlnaClientDiscoveryIntervalHelp": "يحدد الفترة بالثواني بين عمليات بحث SSDP.",
|
||||
"LabelEnableDlnaClientDiscoveryIntervalHelp": "حدد المدة بالثواني بين عمليتي بحث SSDP.",
|
||||
"LabelEnableDlnaDebugLogging": "تفعيل خاصية كشوفات أخطاء DLNA",
|
||||
"LabelEnableDlnaDebugLoggingHelp": "انشاء سجلات كشفية ضخمة ولا ينبغي تفعيلها إلا عند الحاجة إليها بغرض استكشاف الأخطاء وحصرها.",
|
||||
"LabelEnableDlnaPlayTo": "تفعيل خاصية DLNA Play To",
|
||||
"LabelEnableDlnaPlayTo": "تفعيل خاصية 'تشغيل' تحالف الشبكة الرقمية الحية",
|
||||
"LabelEnableDlnaPlayToHelp": "اكتشف الأجهزة على شبكتك ويقدم لك إمكانية التحكم بهم عن بعد.",
|
||||
"LabelEnableDlnaServer": "تفعيل خادم DLNA",
|
||||
"LabelEnableDlnaServerHelp": "يسمح لأجهزة UPnP الموجودة على شبكتك بتصفح المحتوى وتشغيله.",
|
||||
@ -461,10 +461,10 @@
|
||||
"LabelVaapiDeviceHelp": "هذه هي عقدة التصيير التي ستستخدم من قبل التسريع بعتاد الحاسوب.",
|
||||
"LabelValue": "القيمة:",
|
||||
"LabelVersionInstalled": "{0} مثبتة",
|
||||
"LabelXDlnaCap": "سقف X-Dlna:",
|
||||
"LabelXDlnaCapHelp": "تحدد محتوى عنصر X_DLNACAP في النطاق الاسمي ل urn:schemas-dlna-org:device-1-0 .",
|
||||
"LabelXDlnaDoc": "وثيقة X-Dlna:",
|
||||
"LabelXDlnaDocHelp": "تحدد محتوى عنصر X_DLNADOC في النطاق الاسمي ل urn:schemas-dlna-org:device-1-0 .",
|
||||
"LabelXDlnaCap": "معرف قدرة الجهاز:",
|
||||
"LabelXDlnaCapHelp": "حدد محتوى عنصر \"X_DLNACAP\" في مساحة الاسم \"urn: schemas-dlna-org: device-1-0\".",
|
||||
"LabelXDlnaDoc": "معرف فئة الجهاز:",
|
||||
"LabelXDlnaDocHelp": "حدد محتوى عنصر \"X_DLNADOC\" في مساحة الاسم \"urn: schemas-dlna-org: device-1-0\".",
|
||||
"LabelYoureDone": "تم الانتهاء!",
|
||||
"LabelZipCode": "الرمز البريدي:",
|
||||
"LabelffmpegPath": "مسار ffmpeg:",
|
||||
@ -509,7 +509,7 @@
|
||||
"MessageContactAdminToResetPassword": "الرجاء التواصل مع مدير النظام لإعادة أعداد كملة سرّك.",
|
||||
"MessageCreateAccountAt": "أنشئ حساب في {0}",
|
||||
"MessageDeleteTaskTrigger": "هل أنت متأكد أنك تريد حذف زناد المهمة؟",
|
||||
"MessageDirectoryPickerBSDInstruction": "من أجل BSD، يمكنك أن تضبط إعدادات التخزين دخال حساب FreeNAS Jail الخاص بك لكي يتمكن Jellyfin أن يتصل به.",
|
||||
"MessageDirectoryPickerBSDInstruction": "بالنسبة إلى BSD ، قد تحتاج إلى إعداد التخزين داخل \"FreeNAS Jail\" حتى يتمكن Jellyfin من الوصول إلى الوسائط الخاصة بك.",
|
||||
"MessageDirectoryPickerLinuxInstruction": "من أجل أنظمة التشغيل التالية: Linux أو Arch Linux أو CentOS أو Debian أو Fedora أو openSUSE أو Ubuntu، يجب أن تمنح المستخدم النظامي صلاحية القراءة ليتمكن من الوصول إلى أماكن التخزين.",
|
||||
"MessageEnablingOptionLongerScans": "قد يؤدي تمكين هذا الخيار إلى إبطاء البحث في المكتبات بشكل ملحوظ.",
|
||||
"MessageFileReadError": "حصل خطأ أثناء قراءة الملف. الرجاء المحاولة مرة اخرى.",
|
||||
@ -524,7 +524,7 @@
|
||||
"MessageNoPluginsInstalled": "ليس عندك أي ملحقات مثبتة.",
|
||||
"MessageNoTrailersFound": "قم بتثبيت قناة العروض الإعلانية لتحسين متعة المشاهدة بإضافة مكتبة عروض إعلانية من الإنترنت.",
|
||||
"MessageNothingHere": "لا شىء هنا.",
|
||||
"MessagePasswordResetForUsers": "تم إعادة تعيين كلمات المرور للمستخدمين التالين. يمكنهم الآن تسجيل الدخول باستخدام رموز PIN التي تم استخدامها لإجراء إعادة التعيين.",
|
||||
"MessagePasswordResetForUsers": "تم إعادة تعيين كلمات المرور الخاصة بهم للمستخدمين التاليين. يمكنهم الآن تسجيل الدخول باستخدام رموز PIN سهلة الاستخدام التي تم استخدامها لإعادة الضبط.",
|
||||
"MessagePleaseEnsureInternetMetadata": "الرجاء التأكد من أن إمكانية إنزال واصفات البيانات من الإنترنت ممكنة.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "لضبط هذا البرنامج المساعد ، يرجى تسجيل الدخول إلى الخادم المحلي الخاص بك مباشرة.",
|
||||
"MessagePluginInstallDisclaimer": "إن الملحقات التي بناها أعضاء مجتمع Jellyfin لهي طريقة رائعة لتحسين متعة استخدام Jellyfin وذلك بإضافة المزايا والخدمات الجديدة. قبل تثبيت الملحقات، نرجو أخذ العلم بالآثار التي قد تلحقها بخادم Jellyfin الخاص بك، مثل أوقات أطولة لتمشيط مكتبتك، والعمليات الخلفية الإضافية وتقليل استقرار نظامك.",
|
||||
@ -534,7 +534,7 @@
|
||||
"MessageUnsetContentHelp": "المحتوى سيعرض كمجدات اعتيادية. لأفضل النتائج استخدم مدير واصفات البيانات لإعداد نوع محتوى المجلدات الفرعية.",
|
||||
"MessageYouHaveVersionInstalled": "الإصدار المثبت حالياً هو {0}.",
|
||||
"MetadataManager": "مدير واصفات البيانات",
|
||||
"MetadataSettingChangeHelp": "سيؤثر تغيير إعدادات البيانات الوصفية على المحتوى الجديد المُضاف من الآن فصاعدًا. لتحديث المحتوى الحالي ، افتح شاشة التفاصيل وانقر فوق زر التحديث ، أو قم بإجراء تحديثات مجمعة باستخدام مدير البيانات الوصفية.",
|
||||
"MetadataSettingChangeHelp": "سيؤثر تغيير إعدادات البيانات الوصفية على المحتوى الجديد المُضاف من الآن فصاعدًا. لتحديث المحتوى الحالي ، افتح شاشة التفاصيل وانقر فوق الزر \"تحديث\" ، أو قم بإجراء تحديثات مجمعة باستخدام \"مدير البيانات الوصفية\".",
|
||||
"MinutesAfter": "عدد الدقائق اللاحقة",
|
||||
"MinutesBefore": "عدد الدقائق السابقة",
|
||||
"Monday": "الاثنين",
|
||||
@ -608,7 +608,7 @@
|
||||
"OptionOnInterval": "بناء على فترة",
|
||||
"OptionParentalRating": "التصنيف الأبوي",
|
||||
"OptionPlainStorageFolders": "غرض جميع المجلدات كمجلدات تخزين بسيطة",
|
||||
"OptionPlainStorageFoldersHelp": "يتم تمثيل كافة المجلدات في DIDL ك \"object.container.storageFolder\" بدلاً من نوع أكثر تحديدًا ، مثل \"object.container.person.musicArtist\".",
|
||||
"OptionPlainStorageFoldersHelp": "يتم تمثيل جميع المجلدات في DIDL ك \"object.container.storageFolder\" بدلاً من نوع أكثر تحديدًا ، مثل \"object.container.person.musicArtist\".",
|
||||
"OptionPlainVideoItems": "إظهار جميع الفيديوهات كعناصر فيديو بسيطة",
|
||||
"OptionPlainVideoItemsHelp": "يتم تمثيل جميع مقاطع الفيديو في DIDL ك \"object.item.videoItem\" بدلاً من نوع أكثر تحديدًا ، مثل \"object.item.videoItem.movie\".",
|
||||
"OptionPlayCount": "مرات التشغيل",
|
||||
@ -617,8 +617,8 @@
|
||||
"OptionReleaseDate": "تاريخ الإنتاج",
|
||||
"OptionReportByteRangeSeekingWhenTranscoding": "قرّر ما إذا كان الخادم يدعم البحث عن البايت حال التشفير",
|
||||
"OptionReportByteRangeSeekingWhenTranscodingHelp": "هذه مطلوبة لبعض الأجهزة التي لا تحسن البحث في الوقت.",
|
||||
"OptionRequirePerfectSubtitleMatch": "نزّل فقط الترجمات التي توافق بدقة ملفات الفيديو الخاصة بي",
|
||||
"OptionResElement": "عنصر الدقة",
|
||||
"OptionRequirePerfectSubtitleMatch": "قم بتنزيل الترجمات المطابقة تمامًا لملفات الفيديو فقط",
|
||||
"OptionResElement": "المورد",
|
||||
"OptionResumable": "إمكانية التكملة",
|
||||
"OptionSaveMetadataAsHidden": "حفظ واصفات البيانات والصور كملفات مخفية",
|
||||
"OptionSaveMetadataAsHiddenHelp": "سيؤدي تغيير هذا إلى تطبيق البيانات الوصفية الجديدة المحفوظة من الآن فصاعدًا. سيتم تحديث ملفات البيانات الوصفية الموجودة في المرة التالية التي يتم فيها حفظها بواسطة الخادم.",
|
||||
@ -637,7 +637,7 @@
|
||||
"PasswordResetConfirmation": "هل انت متاكد من انك تريد اعادة تعيين كلمة السر؟",
|
||||
"PasswordSaved": "تم حفظ كلمة السر.",
|
||||
"PictureInPicture": "صورة داخل صورة",
|
||||
"PinCodeResetComplete": "تمت إعادة تعيين رمز PIN.",
|
||||
"PinCodeResetComplete": "تمت إعادة تعيين رمز PIN السهل.",
|
||||
"PinCodeResetConfirmation": "هل أنت متأكد أنك تريد إعادة تهيئة الرمز الشخصي؟",
|
||||
"PleaseAddAtLeastOneFolder": "الرجاء إضافة مجلد واحد على الأقل لهذه المكتبة بالضغط على زر \"إضافة\".",
|
||||
"PleaseConfirmPluginInstallation": "الرجاء الضغط على زر موافق لتأكيد قرائتك لما ورد أعلاه وأنك ترغب في الاستمرار في تثبيت الملحق.",
|
||||
@ -673,7 +673,7 @@
|
||||
"TabLogs": "الكشوفات",
|
||||
"TabMusic": "الموسيقى",
|
||||
"TabMyPlugins": "ملحقاتي",
|
||||
"TabNetworks": "الشبكات",
|
||||
"TabNetworks": "شبكات التلفزيون",
|
||||
"TabNfoSettings": "أعدادات Nfo",
|
||||
"TabNotifications": "إشعارات",
|
||||
"TabOther": "أخرى",
|
||||
@ -729,7 +729,7 @@
|
||||
"XmlTvSportsCategoriesHelp": "البرامج من هذه التصنيفات ستعرض كبرامج رياضية. إفصل الإدخالات المتعددة برمز \"|\".",
|
||||
"Yesterday": "البارحة",
|
||||
"ConfirmDeleteImage": "حذف الصورة؟",
|
||||
"ConfigureDateAdded": "قم بتكوين كيفية تحديد \"تاريخ الإضافة\" في لوحة التحكم ضمن اعدادات المكتبة",
|
||||
"ConfigureDateAdded": "قم بإعداد كيفية تحديد البيانات الوصفية ل \"تاريخ الإضافة\" في لوحة المعلومات> المكتبات> إعدادات NFO",
|
||||
"Composer": "ألحان",
|
||||
"CommunityRating": "تقييم الجمهور",
|
||||
"ColorTransfer": "نقل اللون",
|
||||
@ -753,7 +753,7 @@
|
||||
"CancelSeries": "إلغاء المسلسل",
|
||||
"CancelRecording": "إلغاء التسجيل",
|
||||
"ButtonScanAllLibraries": "فحص جميع المكتبات",
|
||||
"ButtonGotIt": "وجدتها",
|
||||
"ButtonGotIt": "حسنا",
|
||||
"ButtonAddImage": "أضف صورة",
|
||||
"BurnSubtitlesHelp": "يحدد ما إذا كان يجب على الخادم نسخ الترجمات المصاحبة عند تحويل ترميز مقاطع الفيديو. سيؤدي تجنب ذلك إلى تحسين الأداء بشكل كبير. حدد تلقائي لنسخ التنسيقات القائمة على الصور (VOBSUB ، و PGS ، و SUB ، و IDX ، ...) وبعض ترجمات ASS أو SSA.",
|
||||
"BoxRear": "العلبة (الجهة الخلفية)",
|
||||
@ -766,7 +766,7 @@
|
||||
"Backdrop": "خلفية الصفحة",
|
||||
"Auto": "تلقائي",
|
||||
"AuthProviderHelp": "إختر مقدم المصادقة ليتم إستخدامه لمصادقة كلمة مرور هذا المستخدم.",
|
||||
"AroundTime": "حول",
|
||||
"AroundTime": "حوالي {0}",
|
||||
"AspectRatio": "نسبة العرض الى الارتفاع",
|
||||
"Ascending": "تصاعدي",
|
||||
"AsManyAsPossible": "أكبر عدد ممكن",
|
||||
@ -789,10 +789,10 @@
|
||||
"Aired": "عرضت",
|
||||
"AirDate": "تاريخ العرض",
|
||||
"AddedOnValue": "تم إضافة {0}",
|
||||
"AddToPlaylist": "إضافة لقائمة التشغيل",
|
||||
"AddToPlayQueue": "إضافة لقائمة التشغيل المؤقتة",
|
||||
"AddToCollection": "إضافة للتجميعات",
|
||||
"Add": "إضافة",
|
||||
"AddToPlaylist": "أضف إلى قائمة التشغيل",
|
||||
"AddToPlayQueue": "أضف إلى قائمة التشغيل المؤقتة",
|
||||
"AddToCollection": "أضف إلى التجميعات",
|
||||
"Add": "اضف",
|
||||
"Actor": "ممثل",
|
||||
"AccessRestrictedTryAgainLater": "الوصول مقيد حاليًا. الرجاء المحاولة لاحقا.",
|
||||
"Absolute": "مطلق",
|
||||
@ -849,7 +849,7 @@
|
||||
"HeaderLiveTvTunerSetup": "اعداد موالف التلفاز المباشر",
|
||||
"HeaderLibrarySettings": "اعدادات المكتبة",
|
||||
"HeaderLibraryOrder": "ترتيب المكتبة",
|
||||
"HeaderKodiMetadataHelp": "لتشغيل او إطفاء البيانات الوصفية بصيغة NFO، عدل احد المكتبات في اعدادات المكتبات واوجد قسم حافظات البيانات الوصفية.",
|
||||
"HeaderKodiMetadataHelp": "لتمكين أو تعطيل البيانات الوصفية ل NFO ، قم بتحرير مكتبة وابحث عن قسم \"حافظات البيانات الوصفية\".",
|
||||
"EnableNextVideoInfoOverlay": "عرض معلومات الفيديو القادم أثناء التشغيل",
|
||||
"DatePlayed": "تاريخ التشغيل",
|
||||
"DateAdded": "تاريخ الإضافة",
|
||||
@ -859,7 +859,7 @@
|
||||
"Artist": "الفنان",
|
||||
"AllowFfmpegThrottling": "إبطاء التحويل",
|
||||
"AlbumArtist": "المؤدي",
|
||||
"Album": "الألبوم",
|
||||
"Album": "ألبوم",
|
||||
"Disconnect": "قطع الاتصال",
|
||||
"Disc": "القرص",
|
||||
"Directors": "المخرجون",
|
||||
@ -1004,7 +1004,7 @@
|
||||
"EnableDecodingColorDepth10Hevc": "تمكين ترميز ال 10 بت عبر العتاد الصلب من اجل HEVC",
|
||||
"LabelFont": "خط:",
|
||||
"LabelFolder": "مجلد:",
|
||||
"LabelIconMaxResHelp": "اعلى دقه للايقونات المعروضة من خلال خاصيه upnp:icon.",
|
||||
"LabelIconMaxResHelp": "عرض الحد الأقصى من دقة الرموز عبر خاصية \"upnp: icon\".",
|
||||
"LabelHomeScreenSectionValue": "الشاشة الرئيسية جزء {0}:",
|
||||
"LabelHomeNetworkQuality": "جودة الشبكة المنزلية:",
|
||||
"LabelBaseUrlHelp": "اضافه مجلد فرعي مخصص لعنوان الخادم. كمثال <code>http://example.com/<b><baseurl></b></code>",
|
||||
@ -1013,8 +1013,8 @@
|
||||
"LabelEnableHttps": "تفعيل HTTPS",
|
||||
"LabelEnableHardwareDecodingFor": "تفعيل فك الترميز عن طريق العتاد الصلب ل:",
|
||||
"LabelCurrentStatus": "الحاله الحالية:",
|
||||
"LabelAlbumArtMaxResHelp": "اقصي مستوي دقة لغطاء الالبوم المكشوف من قبل خاصية upnp:albumArtURI .",
|
||||
"KnownProxiesHelp": "قائمه من عناوين الشبكه المفصولين بفصله للوكلاء المعرفين المستخدمين للاتصال بحاله Jellyfin. هذا الامر مطلوب لاستخدام رؤس صفحات X-Forwarded-For صحيح. يتطلب اعادة التشغيل بعد الحفظ.",
|
||||
"LabelAlbumArtMaxResHelp": "الدقة القصوى لصورة الألبوم المعروضة عبر خاصية \"upnp: AlbumArtURI\".",
|
||||
"KnownProxiesHelp": "قائمة مفصولة بفواصل لعناوين IP أو أسماء المضيفين للخوادم الوكيلة المعروفة المستخدمة عند الاتصال بمثيل Jellyfin الخاص بك. هذا مطلوب للاستفادة المناسبة من رؤوس \"X-Forwarded-For\". يتطلب إعادة التشغيل بعد الحفظ.",
|
||||
"Image": "صورة",
|
||||
"Other": "اخري",
|
||||
"EnableQuickConnect": "تفعيل الاتصال السريع على هذا الخادم",
|
||||
@ -1113,7 +1113,7 @@
|
||||
"PlayNext": "قم بتشغيل التالي",
|
||||
"PlayFromBeginning": "التشغيل من البداية",
|
||||
"PlayCount": "عدد التشغيل",
|
||||
"PlaybackRate": "معدل التشغيل",
|
||||
"PlaybackRate": "سرعة التشغيل",
|
||||
"PlaybackErrorNoCompatibleStream": "هذا العميل غير متوافق مع الوسائط ولا يرسل الخادم تنسيق وسائط متوافق.",
|
||||
"PlaybackData": "معلومات التشغيل",
|
||||
"PlayAllFromHere": "قم بتشغيل كلها من هنا",
|
||||
@ -1330,7 +1330,7 @@
|
||||
"Video": "فيديو",
|
||||
"Vertical": "عمودي",
|
||||
"ValueSeconds": "{0} ثانية",
|
||||
"UseEpisodeImagesInNextUpHelp": "ستستخدم أقسام المتابعة والمتابعة صور الحلقة كصور مصغرة بدلاً من الصورة المصغرة الأساسية للعرض.",
|
||||
"UseEpisodeImagesInNextUpHelp": "سيستخدم قسمي \"التالي\" و \"متابعة المشاهدة\" صور الحلقة كصور مصغرة بدلاً من الصورة المصغرة الأساسية للعرض.",
|
||||
"UseEpisodeImagesInNextUp": "استخدم صور الحلقة في قسمي \"التالي\" و \"متابعة المشاهدة\"",
|
||||
"Upload": "تحميل",
|
||||
"UnsupportedPlayback": "لا يمكن ل Jellyfin فك تشفير المحتوى المحمي بواسطة DRM ولكن سيتم تجربة كل المحتوى بغض النظر ، بما في ذلك العناوين المحمية. قد تظهر بعض الملفات سوداء بالكامل بسبب التشفير أو ميزات أخرى غير مدعومة ، مثل العناوين التفاعلية.",
|
||||
@ -1450,7 +1450,7 @@
|
||||
"LabelKodiMetadataUserHelp": "احفظ بيانات الساعة في ملفات NFO لتستخدمها التطبيقات الأخرى.",
|
||||
"LabelKodiMetadataUser": "حفظ بيانات مشاهدة المستخدم في ملفات NFO من أجل:",
|
||||
"LabelInternetQuality": "جودة الإنترنت:",
|
||||
"LabelDisableCustomCss": "تعطيل سمات / علامات CSS المخصصة المقدمة من الخادم.",
|
||||
"LabelDisableCustomCss": "تعطيل كود CSS المخصص للتسمية / العلامة التجارية المقدمة من الخادم.",
|
||||
"LabelCreateHttpPortMapHelp": "السماح بتعيين المنفذ التلقائي لإنشاء قاعدة لحركة مرور HTTP بالإضافة إلى حركة مرور HTTPS.",
|
||||
"LabelCreateHttpPortMap": "قم بتمكين تعيين المنفذ التلقائي لحركة مرور HTTP و HTTPS.",
|
||||
"LabelAutomaticDiscoveryHelp": "اسمح للتطبيقات باكتشاف Jellyfin تلقائيًا باستخدام منفذ UDP 7359.",
|
||||
@ -1502,7 +1502,7 @@
|
||||
"MediaInfoColorPrimaries": "الألوان الأساسية",
|
||||
"LanNetworksHelp": "قائمة مفصولة بفواصل لعناوين IP أو إدخالات IP / قناع الشبكة للشبكات التي سيتم أخذها في الاعتبار على الشبكة المحلية عند فرض قيود النطاق الترددي. في حالة الضبط ، سيتم اعتبار جميع عناوين IP الأخرى على الشبكة الخارجية وستخضع لقيود النطاق الترددي الخارجي. إذا تُركت فارغة ، فسيتم اعتبار الشبكة الفرعية للخادم فقط على الشبكة المحلية.",
|
||||
"LabelVersion": "إصدار:",
|
||||
"LabelUserRemoteClientBitrateLimitHelp": "تجاوز القيمة العامة الافتراضية المحددة في إعدادات تشغيل الخادم.",
|
||||
"LabelUserRemoteClientBitrateLimitHelp": "تجاوز القيمة العامة الافتراضية المعينة في إعدادات الخادم ، راجع لوحة الاعدادت> التشغيل> تدفق.",
|
||||
"LabelTVHomeScreen": "الشاشة الرئيسية الخاصة بوضع التلفزيون:",
|
||||
"LabelTranscodingProgress": "تقدم التحويل:",
|
||||
"LabelTranscodes": "تحويل الشفرات:",
|
||||
@ -1564,11 +1564,11 @@
|
||||
"LabelAutomaticallyAddToCollection": "إضافة إلى المجموعة تلقائيا",
|
||||
"Console": "وحدة التحكم",
|
||||
"Casual": "غير رسمي",
|
||||
"AllowTonemappingHelp": "يمكن أن يؤدي تعيين النغمة إلى تحويل النطاق الديناميكي لمقطع فيديو من HDR إلى SDR مع الحفاظ على تفاصيل الصورة والألوان ، وهي معلومات مهمة جدًا لتمثيل المشهد الأصلي. يعمل حاليًا فقط عند تحويل ترميز مقاطع الفيديو باستخدام بيانات التعريف HDR10 أو HLG المضمنة. إذا لم يكن التشغيل سلسًا أو فشل ، فيرجى التفكير في إيقاف تشغيل وحدة فك ترميز الأجهزة المقابلة.",
|
||||
"AllowTonemappingHelp": "يمكن أن يؤدي تعيين النغمة إلى تحويل النطاق الديناميكي لمقطع فيديو من HDR إلى SDR مع الحفاظ على تفاصيل الصورة والألوان ، وهي معلومات مهمة جدًا لتمثيل المشهد الأصلي. يعمل حاليًا فقط مع مقاطع فيديو HDR10 أو HLG. يتطلب هذا وقت تشغيل OpenCL أو CUDA المقابل.",
|
||||
"RefFramesNotSupported": "الإطارات المرجعية غير مدعومة",
|
||||
"InterlacedVideoNotSupported": "الفيديو المتشابك غير مدعوم",
|
||||
"AnamorphicVideoNotSupported": "لا يتم دعم الفيديو ذي الصورة المشوهة",
|
||||
"AllowVppTonemappingHelp": "تعين النغمة الكامل للأجهزة دون استخدام مرشح OpenCL. يعمل حاليًا فقط عند تحويل ترميز مقاطع الفيديو باستخدام بيانات تعريف HDR10 المضمنة.",
|
||||
"AllowVppTonemappingHelp": "تعين النغمة الكامل للأجهزة باستخدام Intel. يعمل حاليًا فقط على أجهزة معينة مع مقاطع فيديو HDR10. هذا له أولوية أعلى مقارنة بتطبيق OpenCL آخر.",
|
||||
"EnableVppTonemapping": "تفعيل تعيين نغمة VPP",
|
||||
"Remuxing": "إعادة",
|
||||
"AspectRatioCover": "غلاف",
|
||||
@ -1584,7 +1584,7 @@
|
||||
"TypeOptionPluralBoxSet": "مجموعات مربعه",
|
||||
"TypeOptionPluralBook": "كتب",
|
||||
"TypeOptionPluralAudio": "صوتيات",
|
||||
"TonemappingAlgorithmHelp": "يمكن ضبط النغمة بدقة. إذا لم تكن معتادًا على هذه الخيارات ، فما عليك سوى الاحتفاظ بالخيار الافتراضي. القيمة الموصى بها هي Hable.",
|
||||
"TonemappingAlgorithmHelp": "يمكن ضبط النغمة بدقة. إذا لم تكن معتادًا على هذه الخيارات ، فما عليك سوى الاحتفاظ بالخيار الافتراضي. القيمة الموصى بها هي \"BT.2390\".",
|
||||
"ThumbCard": "بطاقة مصغرة",
|
||||
"Thumb": "ابهام",
|
||||
"Smart": "ذكي",
|
||||
@ -1598,5 +1598,27 @@
|
||||
"LabelTonemappingRange": "نطاق تعيين النغمة:",
|
||||
"LabelTonemappingPeakHelp": "تجاوز الإشارة / الذروة الاسمية / المرجعية بهذه القيمة. يكون مفيدًا عندما تكون معلومات الذروة المضمنة في البيانات الوصفية للعرض غير موثوقة أو عند تعيين درجة اللون من نطاق أقل إلى نطاق أعلى. القيم الموصي بها والافتراضية هي 100 و 0.",
|
||||
"LabelTonemappingPeak": "ذروة رسم الخرائط:",
|
||||
"Cursive": "متّصل"
|
||||
"Cursive": "متّصل",
|
||||
"LabelHardwareEncodingOptions": "خيارات ترميز الأجهزة:",
|
||||
"IntelLowPowerEncHelp": "يمكن أن يحافظ التشفير منخفض الطاقة على مزامنة وحدة المعالجة المركزية (CPU) ووحدة معالجة الرسومات (GPU) غير الضرورية. في Linux ، يجب تعطيلها إذا لم يتم تكوين البرنامج الثابت i915 HuC.",
|
||||
"EnableIntelLowPowerHevcHwEncoder": "قم بتمكين ترميز أجهزة Intel Low-Power HEVC",
|
||||
"EnableIntelLowPowerH264HwEncoder": "قم بتمكين برنامج تشفير الأجهزة Intel Low-Power H.264",
|
||||
"PreferSystemNativeHwDecoder": "تفضل وحدات فك ترميز أجهزة DXVA أو VA-API الأصلية لنظام التشغيل",
|
||||
"ContainerBitrateExceedsLimit": "معدل بت الفيديو تجاوز الحد",
|
||||
"SelectAll": "اختر الكل",
|
||||
"DirectPlayError": "حدث خطأ في بدء التشغيل المباشر",
|
||||
"UnknownAudioStreamInfo": "معلومات دفق الصوت غير معروفة",
|
||||
"UnknownVideoStreamInfo": "معلومات دفق الفيديو غير معروفة",
|
||||
"VideoBitrateNotSupported": "معدل بت الفيديو غير مدعوم",
|
||||
"AudioIsExternal": "دفق الصوت خارجي",
|
||||
"ThemeVideo": "فيديو الشارة",
|
||||
"ThemeSong": "أغنية الشارة",
|
||||
"Sample": "عيّنة",
|
||||
"Scene": "المشاهد",
|
||||
"Interview": "مقابلة",
|
||||
"DeletedScene": "مشاهد محذوفة",
|
||||
"BehindTheScenes": "وراء الكواليس",
|
||||
"Trailer": "العرض الإعلاني",
|
||||
"Clip": "ميزة",
|
||||
"ButtonExitApp": "أغلق التطبيق"
|
||||
}
|
||||
|
@ -24,5 +24,8 @@
|
||||
"Add": "Дадаць",
|
||||
"Actor": "Акцёр",
|
||||
"AccessRestrictedTryAgainLater": "У цяперашні час доступ абмежаваны. Калі ласка паспрабуйце зноў пазней.",
|
||||
"Absolute": "Абсалютны"
|
||||
"Absolute": "Абсалютны",
|
||||
"Small": "Маленькі",
|
||||
"Normal": "Нармальны",
|
||||
"Large": "Вялікі"
|
||||
}
|
||||
|
@ -135,7 +135,7 @@
|
||||
"HeaderEditImages": "Редактиране на изображенията",
|
||||
"HeaderEnabledFields": "Включени полета",
|
||||
"HeaderError": "Грешка",
|
||||
"HeaderFeatureAccess": "Достъп до функции",
|
||||
"HeaderFeatureAccess": "Достъп до функции:",
|
||||
"HeaderFetchImages": "Свали изображения:",
|
||||
"HeaderForKids": "Детски",
|
||||
"HeaderFrequentlyPlayed": "Често пускани",
|
||||
@ -176,7 +176,7 @@
|
||||
"HeaderProfileInformation": "Профил",
|
||||
"HeaderProfileServerSettingsHelp": "Тези величини определят как Джелифин сървърът ще се представя на устройствата.",
|
||||
"HeaderRecentlyPlayed": "Скоро пускани",
|
||||
"HeaderRemoteControl": "Отдалечен контрол",
|
||||
"HeaderRemoteControl": "Отдалечен контрол:",
|
||||
"HeaderRemoveMediaFolder": "Премахване на медийна папка",
|
||||
"HeaderResponseProfile": "Профил на отговора",
|
||||
"HeaderRevisionHistory": "Списък с промени",
|
||||
@ -797,7 +797,7 @@
|
||||
"HeaderAppearsOn": "Фигурира в",
|
||||
"ApiKeysCaption": "Списък с работещите в момента API ключове",
|
||||
"HeaderApiKeysHelp": "Външните програми се налага да имат API ключ ,за да комуникират правилно със сървъра.Такива се издават при вписването в сървъра или чрез ръчно предоставяне.",
|
||||
"HeaderAllowMediaDeletionFrom": "Позволи изтриването на медия от",
|
||||
"HeaderAllowMediaDeletionFrom": "Позволи изтриването на медия от:",
|
||||
"HeaderAlert": "Предупреждение",
|
||||
"HeaderAccessScheduleHelp": "Създай разписание за достъп ,за да го ограничиш до определени часове.",
|
||||
"HeaderAccessSchedule": "Разписание за достъп",
|
||||
@ -820,7 +820,7 @@
|
||||
"HeaderXmlDocumentAttribute": "Атрибут на XML документа",
|
||||
"HeaderUpcomingOnTV": "Скоро по ТВ",
|
||||
"HeaderTypeText": "Въведи текст",
|
||||
"HeaderTypeImageFetchers": "Извличане на картини ({0})",
|
||||
"HeaderTypeImageFetchers": "Извличане на картини ({0}):",
|
||||
"HeaderTuners": "Тунери",
|
||||
"HeaderTranscodingProfileHelp": "Добави профили за транскодиране ,за да се види кои формати ще се използват ,когато е необходимо транскодиране.",
|
||||
"HeaderThisUserIsCurrentlyDisabled": "Този потребител в момента е блокиран",
|
||||
@ -849,7 +849,7 @@
|
||||
"HeaderRecordingOptions": "Настройки за запис",
|
||||
"HeaderPluginInstallation": "Инсталиране на добавка",
|
||||
"HeaderPlaybackError": "Грешка при възпроизвеждане",
|
||||
"HeaderPlayback": "Възпроизвеждане на медия",
|
||||
"HeaderPlayback": "Възпроизвеждане на медия:",
|
||||
"HeaderPinCodeReset": "Зануляване на пин код",
|
||||
"HeaderPhotoAlbums": "Фото албум",
|
||||
"HeaderPasswordReset": "Зануляване на парола",
|
||||
@ -1259,7 +1259,7 @@
|
||||
"OptionRegex": "Регуларен",
|
||||
"OptionRandom": "Случаен",
|
||||
"OptionProtocolHttp": "HTTP",
|
||||
"OptionProtocolHls": "Директно предаване по HTTP",
|
||||
"OptionProtocolHls": "Директно предаване по HTTP (HLS)",
|
||||
"OptionPlainVideoItemsHelp": "Всички видеофайлове са представени в DIDL като \"object.item.videoItem\" вместо по-конкретен тип, като например \"object.item.videoItem.movie\".",
|
||||
"OptionPlainStorageFoldersHelp": "Всички папки са представени в DIDL като \"object.container.storageFolder\" вместо по-конкретен тип, като например \"object.container.person.musicArtist\".",
|
||||
"OptionMax": "Максимално",
|
||||
|
@ -130,5 +130,6 @@
|
||||
"Depressed": "অবনমিত",
|
||||
"DeleteUserConfirmation": "আপনি কি নিশ্চিত যে আপনি এই ব্যবহারকারীকে মুছতে চান?",
|
||||
"DeleteUser": "ব্যবহারকারী মুছুন",
|
||||
"DeleteMedia": "মিডিয়া মুছুন"
|
||||
"DeleteMedia": "মিডিয়া মুছুন",
|
||||
"AllowedRemoteAddressesHelp": "নেটওয়ার্কের জন্য যেই আইপি এড্রেসগুলো অথবা আইপি/নেটমাস্ক গুলো কমা (,) দিয়ে আলাদা করা শুধু সেই এন্ট্রিগুলো রিমোটলি কানেক্ট হতে পারবে। যদি খালি রাখা হয় তাহলে সব আইপি থেকে কানেক্ট হতে পারবে।"
|
||||
}
|
||||
|
@ -125,7 +125,7 @@
|
||||
"HeaderEditImages": "Edita Imatges",
|
||||
"HeaderEnabledFields": "Camps Habilitats",
|
||||
"HeaderExternalIds": "Identificadors externs:",
|
||||
"HeaderFeatureAccess": "Accés a Funcions",
|
||||
"HeaderFeatureAccess": "Accés a Funcions:",
|
||||
"HeaderFetchImages": "Obtingues Imatges:",
|
||||
"HeaderFrequentlyPlayed": "Reproduït Freqüentment",
|
||||
"HeaderHttpHeaders": "Capçaleres HTTP",
|
||||
@ -168,7 +168,7 @@
|
||||
"HeaderProfileServerSettingsHelp": "Aquests valors controlen com el servidor es presentarà als clients.",
|
||||
"HeaderRecentlyPlayed": "Reproduït Recentment",
|
||||
"HeaderRecordingOptions": "Opcions d'Enregistrament",
|
||||
"HeaderRemoteControl": "Control Remot",
|
||||
"HeaderRemoteControl": "Control Remot:",
|
||||
"HeaderRunningTasks": "Tasques Corrent",
|
||||
"HeaderScenes": "Escenes",
|
||||
"HeaderSeasons": "Temporades",
|
||||
@ -592,7 +592,7 @@
|
||||
"Channels": "Canals",
|
||||
"Collections": "Col·leccions",
|
||||
"Favorites": "Preferits",
|
||||
"HeaderAlbumArtists": "Àlbum de l'artista",
|
||||
"HeaderAlbumArtists": "Artistes de l'àlbum",
|
||||
"ChannelNumber": "Número de canal",
|
||||
"Categories": "Categories",
|
||||
"ButtonWebsite": "Lloc web",
|
||||
@ -793,7 +793,7 @@
|
||||
"HeaderAudioBooks": "Llibres d’àudio",
|
||||
"HeaderAppearsOn": "Apareix a",
|
||||
"HeaderApp": "App",
|
||||
"HeaderAllowMediaDeletionFrom": "Permetre la supressió de mitjans des de",
|
||||
"HeaderAllowMediaDeletionFrom": "Permetre la supressió de mitjans des de:",
|
||||
"HeaderAlert": "Alerta",
|
||||
"HeaderAddUser": "Afegir usuari",
|
||||
"HeaderAddUpdateSubtitle": "Afegir/ Actualitzar subtítols",
|
||||
@ -1318,7 +1318,7 @@
|
||||
"LabelCertificatePassword": "Verificar contrasenya:",
|
||||
"LabelBurnSubtitles": "Gravar subtítols:",
|
||||
"LabelBlockContentWithTags": "Bloquejar elements amb etiquetes:",
|
||||
"LabelBlastMessageIntervalHelp": "Determina la durada en segons entre forns missatges vius.",
|
||||
"LabelBlastMessageIntervalHelp": "Determina la durada en segons entre ràfegues de missatges en directe.",
|
||||
"LabelBlastMessageInterval": "Interval entre missatges de vida:",
|
||||
"LabelBitrate": "Tassa de bits:",
|
||||
"LabelBindToLocalNetworkAddressHelp": "Anul·lar l'adreça IP local per al servidor HTTP. Si és buit, el servidor s'unirà a totes les adreces disponibles. El canvi d'aquest valor, es requereix un reinici.",
|
||||
@ -1362,7 +1362,7 @@
|
||||
"HeaderVideos": "vídeos",
|
||||
"HeaderVideoQuality": "Qualitat de vídeo",
|
||||
"HeaderUploadSubtitle": "Pujar subtítols",
|
||||
"HeaderTypeImageFetchers": "Buscadors d'imatge ({0})",
|
||||
"HeaderTypeImageFetchers": "Buscadors d'imatge ({0}):",
|
||||
"HeaderTuners": "Sintonitzadors",
|
||||
"HeaderTunerDevices": "Dispositius sintonitzadors",
|
||||
"HeaderTranscodingProfileHelp": "Afegir la transcodificació de perfils per indicar quins formats han de ser utilitzats quan es requereix la transcodificació.",
|
||||
@ -1513,7 +1513,7 @@
|
||||
"HeaderPortRanges": "Firewall i configuració del Proxy",
|
||||
"HeaderPluginInstallation": "Instal·lació plug-in",
|
||||
"HeaderPlayOn": "Reproduir a",
|
||||
"HeaderPlayback": "Suport de reproducció",
|
||||
"HeaderPlayback": "Suport de reproducció:",
|
||||
"HeaderPinCodeReset": "Restablir PIN Code",
|
||||
"HeaderPhotoAlbums": "Àlbum de fotos",
|
||||
"HeaderOtherItems": "Altres elements",
|
||||
@ -1548,5 +1548,37 @@
|
||||
"LabelSyncPlaySettingsDescription": "Canvia les preferències de SyncPlay",
|
||||
"LabelMaxDaysForNextUp": "Màxims dies per \"A continuació\":",
|
||||
"LabelHardwareEncoding": "Codificació per Hardware:",
|
||||
"ErrorPlayerNotFound": "No s'ha trobat cap reproductor per al fitxer multimèdia sol·licitat."
|
||||
"ErrorPlayerNotFound": "No s'ha trobat cap reproductor per al fitxer multimèdia sol·licitat.",
|
||||
"SelectAll": "Selecciona-ho tot",
|
||||
"DirectPlayError": "Hi ha hagut un error en iniciar la reproducció en directe",
|
||||
"UnknownAudioStreamInfo": "La informació del flux d'àudio és desconeguda",
|
||||
"UnknownVideoStreamInfo": "La informació del flux de vídeo és desconeguda",
|
||||
"VideoBitrateNotSupported": "La velocitat de transmissió del vídeo no està suportada",
|
||||
"LabelHardwareEncodingOptions": "Opcions de codificació per maquinari:",
|
||||
"EnableIntelLowPowerHevcHwEncoder": "Habilita el còdec per maquinari Intel Low-Power HEVC",
|
||||
"EnableIntelLowPowerH264HwEncoder": "Habilita el còdec per maquinari Intel Low-Power H.264",
|
||||
"ContainerBitrateExceedsLimit": "El còdec de vídeo excedeix el límit",
|
||||
"TypeOptionPluralVideo": "Vídeos",
|
||||
"TypeOptionPluralSeason": "Temporades",
|
||||
"TypeOptionPluralMusicVideo": "Vídeos Musicals",
|
||||
"TypeOptionPluralMusicArtist": "Artistes musicals",
|
||||
"TypeOptionPluralMusicAlbum": "Àlbums de música",
|
||||
"TypeOptionPluralMovie": "Pel·lícules",
|
||||
"TypeOptionPluralEpisode": "Episodis",
|
||||
"TypeOptionPluralBook": "Llibres",
|
||||
"Track": "Pista",
|
||||
"Print": "Imprimeix",
|
||||
"PreviousChapter": "Episodi anterior",
|
||||
"PlaybackErrorPlaceHolder": "Això és un text variable per a mitjans físics que Jellyfin no pot reproduir. Si us plau insereix el disc per a reproduir.",
|
||||
"OtherArtist": "Altres artistes",
|
||||
"NextChapter": "Pròxim episodi",
|
||||
"Mixer": "Mesclador",
|
||||
"MediaInfoTitle": "Títol",
|
||||
"LabelSyncPlaySettingsSyncCorrection": "Correcció de sincronisme",
|
||||
"LabelSyncPlaySettingsExtraTimeOffset": "Temps afegit de decalatge:",
|
||||
"LabelSortName": "Ordenar per nom:",
|
||||
"LabelAutomaticallyAddToCollectionHelp": "Quan, com a mínim dues pel·lícules, tenen el mateix nom de col·lecció, seran afegides a la col·lecció.",
|
||||
"LabelAutomaticallyAddToCollection": "Afegir a la col·lecció automàticament",
|
||||
"Cursive": "Cursiva",
|
||||
"Console": "Terminal"
|
||||
}
|
||||
|
@ -77,7 +77,7 @@
|
||||
"CancelRecording": "Zrušit nahrávání",
|
||||
"CancelSeries": "Ukončit Seriál",
|
||||
"Categories": "Kategorie",
|
||||
"ChannelAccessHelp": "Vyberte kanály, které chcete sdílet s tímto uživatelem. Administrátoři budou moci upravovat všechny kanály pomocí správce metadat.",
|
||||
"ChannelAccessHelp": "Vyberte kanály, které chcete sdílet s tímto uživatelem. Administrátoři budou moci upravovat všechny kanály pomocí Správce metadat.",
|
||||
"ChannelNameOnly": "Kanál {0} jen",
|
||||
"ChannelNumber": "Číslo kanálu",
|
||||
"CinemaModeConfigurationHelp": "Tento režim přibližuje domácí sledování filmů zážitku v kině díky možnosti přehrát upoutávky k filmům a vlastní úvodní video před hlavním pořadem.",
|
||||
@ -227,7 +227,7 @@
|
||||
"HeaderEnabledFields": "Povolené pole",
|
||||
"HeaderEnabledFieldsHelp": "Zrušte zaškrtnutí, abyste zabránili změnám dat.",
|
||||
"HeaderError": "Chyba",
|
||||
"HeaderFeatureAccess": "Přístup k funkcím",
|
||||
"HeaderFeatureAccess": "Přístup k funkcím:",
|
||||
"HeaderFetchImages": "Načíst obrázky:",
|
||||
"HeaderForKids": "Pro děti",
|
||||
"HeaderFrequentlyPlayed": "Nejčastěji přehráváno",
|
||||
@ -270,9 +270,9 @@
|
||||
"HeaderPassword": "Heslo",
|
||||
"HeaderPasswordReset": "Obnova hesla",
|
||||
"HeaderPaths": "Cesty",
|
||||
"HeaderPinCodeReset": "Obnovit PIN kód",
|
||||
"HeaderPinCodeReset": "Obnovit Easy PIN kód",
|
||||
"HeaderPlayAll": "Přehrát vše",
|
||||
"HeaderPlayback": "Přehrání média",
|
||||
"HeaderPlayback": "Přehrávání médií:",
|
||||
"HeaderPlaybackError": "Chyba přehrávání",
|
||||
"HeaderPleaseSignIn": "Prosíme, přihlaste se",
|
||||
"HeaderPluginInstallation": "Instalace zásuvných modulů",
|
||||
@ -282,7 +282,7 @@
|
||||
"HeaderRecentlyPlayed": "Naposledy přehráváno",
|
||||
"HeaderRecordingOptions": "Nastavení nahrávání",
|
||||
"HeaderRecordingPostProcessing": "Následné zpracování nahrávek",
|
||||
"HeaderRemoteControl": "Dálkový ovladač",
|
||||
"HeaderRemoteControl": "Dálkový ovladač:",
|
||||
"HeaderRemoveMediaFolder": "Odebrat složku médií",
|
||||
"HeaderRemoveMediaLocation": "Odebrat umístění media",
|
||||
"HeaderResponseProfile": "Profil pro odezvy",
|
||||
@ -355,7 +355,7 @@
|
||||
"LabelAirsAfterSeason": "Vysíláno po sezóně:",
|
||||
"LabelAirsBeforeEpisode": "Vysíláno před epizodou:",
|
||||
"LabelAirsBeforeSeason": "Vysíláno před sezónou:",
|
||||
"LabelAlbumArtHelp": "PN používá obrázek alba v rámci technologie dlna:profileID atributu upnp:albumArtURI. Někteří klienti vyžadují konkrétní hodnoty, bez ohledu na velikost obrázku.",
|
||||
"LabelAlbumArtHelp": "PN používá obrázek alba v rámci technologie 'dlna:profileID' atributu 'upnp:albumArtURI'. Někteří klienti vyžadují konkrétní hodnoty, bez ohledu na velikost obrázku.",
|
||||
"LabelAlbumArtMaxHeight": "Maximální výška alba:",
|
||||
"LabelAlbumArtMaxWidth": "Maximální výška alba:",
|
||||
"LabelAlbumArtPN": "Alba PN:",
|
||||
@ -383,7 +383,7 @@
|
||||
"LabelCriticRating": "Hodnocení kritiků:",
|
||||
"LabelCurrentPassword": "Aktuální heslo:",
|
||||
"LabelCustomCss": "Vlastní CSS:",
|
||||
"LabelCustomCssHelp": "Aplikovat vaše vlastní styly webového rozhraní.",
|
||||
"LabelCustomCssHelp": "Aplikovat vaše vlastní styly webového rozhraní pro změnu vzhledu či brandingu.",
|
||||
"LabelCustomDeviceDisplayNameHelp": "Nahradit vlastním názvem zobrazení nebo ponechte prázdné, aby název byl určen zařízením.",
|
||||
"LabelCustomRating": "Vlastní hodnocení:",
|
||||
"LabelDashboardTheme": "Motiv nástěnky serveru:",
|
||||
@ -419,7 +419,7 @@
|
||||
"LabelEnableDlnaClientDiscoveryIntervalHelp": "Určuje interval mezi dvěma vyhledáváními SSDP.",
|
||||
"LabelEnableDlnaDebugLogging": "Povolit DLNA protokolování (pro ladění)",
|
||||
"LabelEnableDlnaDebugLoggingHelp": "Vytváří velké soubory se záznamy a doporučuje se používat pouze pro potřeby odstraňování problémů.",
|
||||
"LabelEnableDlnaPlayTo": "Povolit DLNA přehrávání",
|
||||
"LabelEnableDlnaPlayTo": "Povolit funkci DLNA 'Play To'",
|
||||
"LabelEnableDlnaPlayToHelp": "Umí detekovat zařízení v rámci vaší sítě a nabízí možnost jejich dálkového ovládání.",
|
||||
"LabelEnableDlnaServer": "Povolit DLNA server",
|
||||
"LabelEnableDlnaServerHelp": "Umožnit zařízením UPnP v síti procházet a přehrávat obsah.",
|
||||
@ -455,8 +455,8 @@
|
||||
"LabelImageFetchersHelp": "Povolí řazení stahovačů obrázků dle priority.",
|
||||
"LabelImageType": "Typ obrázku:",
|
||||
"LabelImportOnlyFavoriteChannels": "Zamezit označení kanálů jako oblíbené",
|
||||
"LabelInNetworkSignInWithEasyPassword": "Povolit přihlášení snadným PIN kódem uvnitř lokální sítě",
|
||||
"LabelInNetworkSignInWithEasyPasswordHelp": "Pomocí jednoduchého kódu PIN se přihlaste ke klientům v místní síti. Vaše běžné heslo bude potřeba pouze mimo domov. Pokud je kód PIN ponechán prázdný, nebudete potřebovat heslo v domácí síti.",
|
||||
"LabelInNetworkSignInWithEasyPassword": "Povolit přihlášení Easy PIN kódem uvnitř lokální sítě",
|
||||
"LabelInNetworkSignInWithEasyPasswordHelp": "Pomocí Easy PIN kódu se přihlaste ke klientům v místní síti. Vaše běžné heslo bude potřeba pouze mimo domov. Pokud je kód PIN ponechán prázdný, nebudete potřebovat heslo v domácí síti.",
|
||||
"LabelKeepUpTo": "Aktualizovat k:",
|
||||
"LabelKidsCategories": "Dětské kategorie:",
|
||||
"LabelKodiMetadataDateFormat": "Formát data vydání:",
|
||||
@ -493,7 +493,7 @@
|
||||
"LabelMetadataPathHelp": "Zadejte vlastní umístění pro stažení obrázků a metadat.",
|
||||
"LabelMetadataReaders": "Čtečky metadat:",
|
||||
"LabelMetadataReadersHelp": "Seřaďte své preferované lokální zdroje metadat dle priority. První nalezená data budou načtena.",
|
||||
"LabelMetadataSavers": "Střadatelé metadat:",
|
||||
"LabelMetadataSavers": "Ukládání metadat:",
|
||||
"LabelMetadataSaversHelp": "Vyberte formáty souborů, které chcete použít pro ukládání metadat.",
|
||||
"LabelMethod": "Metoda:",
|
||||
"LabelMinBackdropDownloadWidth": "Maximální šířka pro stažení pozadí:",
|
||||
@ -565,7 +565,7 @@
|
||||
"LabelScheduledTaskLastRan": "Poslední spuštění {0}, zabralo {1}.",
|
||||
"LabelScreensaver": "Šetřič obrazovky:",
|
||||
"LabelSeasonNumber": "Číslo sezóny:",
|
||||
"LabelSelectFolderGroups": "Automaticky seskupit obsah z následujících složek do zobrazení, jako jsou Filmy, Hudba a TV:",
|
||||
"LabelSelectFolderGroups": "Automaticky seskupit obsah z následujících složek do zobrazení, jako jsou 'Filmy', 'Hudba' a 'Seriály':",
|
||||
"LabelSelectFolderGroupsHelp": "Složky, které nejsou zaškrtnuty budou zobrazeny ve vlastním pohledu.",
|
||||
"LabelSelectUsers": "Vyberte uživatele:",
|
||||
"LabelSelectVersionToInstall": "Vyber verzi k instalaci:",
|
||||
@ -580,7 +580,7 @@
|
||||
"LabelSkipIfGraphicalSubsPresent": "Přeskočit, jestliže video obsahuje vložené titulky",
|
||||
"LabelSkipIfGraphicalSubsPresentHelp": "Ponecháním textových titulků je možné dosáhnout efektivnějšího přenosu videa a snížení pravděpodobnosti, že bude video nutné překódovat.",
|
||||
"LabelSonyAggregationFlags": "Agregační příznaky Sony:",
|
||||
"LabelSonyAggregationFlagsHelp": "Určuje obsah prvku aggregationFlags ve jmenném prostoru urn:schemas-sonycom:av.",
|
||||
"LabelSonyAggregationFlagsHelp": "Určuje obsah prvku 'aggregationFlags' ve jmenném prostoru 'urn:schemas-sonycom:av'.",
|
||||
"LabelSortTitle": "Třídit dle názvu:",
|
||||
"LabelSource": "Zdroj:",
|
||||
"LabelSportsCategories": "Sportovní kategorie:",
|
||||
@ -617,10 +617,10 @@
|
||||
"LabelValue": "Hodnota:",
|
||||
"LabelVersion": "Verze:",
|
||||
"LabelVersionInstalled": "{0} instalováno",
|
||||
"LabelXDlnaCap": "Zachytávací zařízení X-DLNA:",
|
||||
"LabelXDlnaCapHelp": "Určuje obsah prvku X_DLNACAP ve jmenném prostoru urn:schemas-dlna-org:device-1-0.",
|
||||
"LabelXDlnaDoc": "Dokumentace X-DLNA:",
|
||||
"LabelXDlnaDocHelp": "Určuje obsah prvku X_DLNADOC ve jmenném prostoru urn:schemas-dlna-org:device-1-0.",
|
||||
"LabelXDlnaCap": "ID schopnosti zařízení:",
|
||||
"LabelXDlnaCapHelp": "Určuje obsah prvku 'X_DLNACAP' ve jmenném prostoru 'urn:schemas-dlna-org:device-1-0'.",
|
||||
"LabelXDlnaDoc": "ID třídy zařízení:",
|
||||
"LabelXDlnaDocHelp": "Určuje obsah prvku 'X_DLNADOC' ve jmenném prostoru 'urn:schemas-dlna-org:device-1-0'.",
|
||||
"LabelYear": "Rok:",
|
||||
"LabelYoureDone": "Hotovo!",
|
||||
"LabelZipCode": "PSČ:",
|
||||
@ -629,7 +629,7 @@
|
||||
"Large": "Velký",
|
||||
"LatestFromLibrary": "Nejnovější {0}",
|
||||
"LearnHowYouCanContribute": "Zjistěte, jak můžete přispět.",
|
||||
"LibraryAccessHelp": "Vyberte knihovny, které chcete sdílet s tímto uživatelem. Administrátoři budou moci editovat všechny složky pomocí správce metadat.",
|
||||
"LibraryAccessHelp": "Vyberte knihovny, které chcete sdílet s tímto uživatelem. Administrátoři budou moci editovat všechny složky pomocí Správce metadat.",
|
||||
"List": "Seznam",
|
||||
"Live": "Živě",
|
||||
"LiveBroadcasts": "Přímé přenosy",
|
||||
@ -692,7 +692,7 @@
|
||||
"MessageNoPluginsInstalled": "Nemáte instalovány žádné zásuvné moduly.",
|
||||
"MessageNoTrailersFound": "Chcete-li si zlepšit zážitek ze sledování, nainstalujte si kanál s upoutávkami.",
|
||||
"MessageNothingHere": "Tady nic není.",
|
||||
"MessagePasswordResetForUsers": "Obnovení hesla bylo provedeno následujícími uživateli. Nyní se mohou přihlásit pomocí kódů PIN, které byly použity k provedení resetu.",
|
||||
"MessagePasswordResetForUsers": "Následujícím uživatelům bylo resetováno heslo. Nyní se mohou přihlásit pomocí kódů Easy PIN, které byly použity k provedení resetu.",
|
||||
"MessagePlayAccessRestricted": "Přehrávání tohoto obsahu je aktuálně omezeno. Další informace získáte od správce serveru.",
|
||||
"MessagePleaseEnsureInternetMetadata": "Prosím zkontrolujte, zda máte povoleno stahování metadat z internetu.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Pro konfiguraci zásuvného modulu se přihlaste přímo na lokální server.",
|
||||
@ -700,10 +700,10 @@
|
||||
"MessageReenableUser": "Viz níže pro znovuzapnutí",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "Z vaší knihovny budou odstraněny následující zdroje médií:",
|
||||
"MessageUnableToConnectToServer": "Nejsme schopni se připojit k vybranému serveru právě teď. Prosím, ujistěte se, že je spuštěn a zkuste to znovu.",
|
||||
"MessageUnsetContentHelp": "Obsah je zobrazen pomocí prostých složek. Pro dosažení nejlepších výsledků pomocí správce metadat nastavte typy obsahu pod-složek.",
|
||||
"MessageUnsetContentHelp": "Obsah je zobrazen pomocí prostých složek. Pro dosažení nejlepších výsledků pomocí Správce metadat nastavte typy obsahu pod-složek.",
|
||||
"MessageYouHaveVersionInstalled": "V současné době máte instalovánu verzi {0}.",
|
||||
"MetadataManager": "Manažer metadat",
|
||||
"MetadataSettingChangeHelp": "Změna nastavení metadat bude mít vliv na obsah, který bude nově přidán v budoucnu. Chcete-li aktualizovat stávající obsah, otevřete obrazovku s podrobnostmi a klikněte na tlačítko Aktualizovat, nebo proveďte hromadnou aktualizaci pomocí správce metadat.",
|
||||
"MetadataManager": "Správce metadat",
|
||||
"MetadataSettingChangeHelp": "Změna nastavení metadat bude mít vliv na obsah, který bude nově přidán v budoucnu. Chcete-li aktualizovat stávající obsah, otevřete obrazovku s podrobnostmi a klikněte na tlačítko 'Aktualizovat', nebo proveďte hromadnou aktualizaci pomocí Správce metadat.",
|
||||
"MinutesAfter": "minut po",
|
||||
"MinutesBefore": "minut předem",
|
||||
"Mobile": "Mobilní",
|
||||
@ -782,7 +782,7 @@
|
||||
"OptionEveryday": "Každý den",
|
||||
"OptionExternallyDownloaded": "Externí stažení",
|
||||
"OptionExtractChapterImage": "Povolit extrakci obrázků z videa",
|
||||
"OptionHasThemeSong": "Tematická hudba",
|
||||
"OptionHasThemeSong": "Znělka",
|
||||
"OptionHasThemeVideo": "Tematické video",
|
||||
"OptionHideUser": "Skrýt tohoto uživatele z přihlašovacích obrazovek",
|
||||
"OptionHideUserFromLoginHelp": "Vhodné pro soukromé a administrátorské účty. Pro přihlášení musí uživatel manuálně zadat uživatelské jméno a heslo.",
|
||||
@ -796,16 +796,16 @@
|
||||
"OptionOnInterval": "V intervalu",
|
||||
"OptionParentalRating": "Rodičovské hodnocení",
|
||||
"OptionPlainStorageFolders": "Zobrazit všechny složky jako obyčejné složky pro ukládání",
|
||||
"OptionPlainStorageFoldersHelp": "Všechny složky jsou prezentovány v DIDL jako \"object.container.storageFolder\" místo konkrétnějšího typu, například \"object.container.person.musicArtist\".",
|
||||
"OptionPlainStorageFoldersHelp": "Všechny složky jsou prezentovány v DIDL jako 'object.container.storageFolder' místo konkrétnějšího typu, například 'object.container.person.musicArtist'.",
|
||||
"OptionPlainVideoItems": "Zobrazit všechna videa jako s obyčejné video položky",
|
||||
"OptionPlainVideoItemsHelp": "Všechna videa jsou prezentována v DIDL jako \"object.item.videoItem\" místo konkrétnějšího typu, například \"object.item.videoItem.movie\".",
|
||||
"OptionPlainVideoItemsHelp": "Všechna videa jsou prezentována v DIDL jako 'object.item.videoItem' místo konkrétnějšího typu, například 'object.item.videoItem.movie'.",
|
||||
"OptionPlayCount": "Počet přehrání",
|
||||
"OptionPremiereDate": "Datum premiéry",
|
||||
"OptionRegex": "Regexp",
|
||||
"OptionReleaseDate": "Datum vydání",
|
||||
"OptionReportByteRangeSeekingWhenTranscoding": "Hlásit, že server podporuje vyhledávání bajtů při překódování",
|
||||
"OptionReportByteRangeSeekingWhenTranscodingHelp": "Tento krok je nutný pro některá zařízení, které nemají moc dobrý time seek.",
|
||||
"OptionRequirePerfectSubtitleMatch": "Stahovat jen titulky, které perfektně sedí k mým video souborům",
|
||||
"OptionRequirePerfectSubtitleMatch": "Stahovat jen titulky, které perfektně sedí k video souborům",
|
||||
"OptionResElement": "'res' element",
|
||||
"OptionResumable": "Pozastavavitelný",
|
||||
"OptionSaveMetadataAsHidden": "Ukládat metadata a obrázky jako skryté soubory",
|
||||
@ -833,8 +833,8 @@
|
||||
"PerfectMatch": "Přesná shoda",
|
||||
"Photos": "Fotky",
|
||||
"PictureInPicture": "Obraz v obraze",
|
||||
"PinCodeResetComplete": "PIN kód byl obnoven.",
|
||||
"PinCodeResetConfirmation": "Jsou si jist, že chcete resetovat PIN kód?",
|
||||
"PinCodeResetComplete": "Kód Easy PIN byl resetován.",
|
||||
"PinCodeResetConfirmation": "Opravdu chcete resetovat kód Easy PIN?",
|
||||
"PlaceFavoriteChannelsAtBeginning": "Umístit oblíbené kanály na začátek",
|
||||
"Play": "Přehrát",
|
||||
"PlayAllFromHere": "Přehrát vše odsud",
|
||||
@ -845,7 +845,7 @@
|
||||
"PlaybackErrorNoCompatibleStream": "Tento klient není kompatibilní s médiem a server neodesílá kompatibilní formát médií.",
|
||||
"Played": "Přehráno",
|
||||
"Playlists": "Seznamy skladeb",
|
||||
"PleaseAddAtLeastOneFolder": "Přidejte prosím nejméně jednu složku do této knihovny pomocí tlačítka Přidat.",
|
||||
"PleaseAddAtLeastOneFolder": "Přidejte prosím nejméně jednu složku do této knihovny pomocí tlačítka '+' v sekci 'Složky'.",
|
||||
"PleaseConfirmPluginInstallation": "Pro potvrzení, že jste si přečetli text výše a chcete pokračovat v instalaci zásuvných modulů, klikněte na tlačítko OK.",
|
||||
"PleaseEnterNameOrId": "Prosím, zadejte název nebo externí Id.",
|
||||
"PleaseRestartServerName": "Prosím restartuje Jellyfin na serveru {0}.",
|
||||
@ -1056,7 +1056,7 @@
|
||||
"General": "Obecné",
|
||||
"Genre": "Žánr",
|
||||
"GroupBySeries": "Seskupit podle série",
|
||||
"HeaderAllowMediaDeletionFrom": "Povolit smazání médií z",
|
||||
"HeaderAllowMediaDeletionFrom": "Povolit mazání médií z:",
|
||||
"HeaderAppearsOn": "Objeví se",
|
||||
"HeaderBlockItemsWithNoRating": "Blokovat položky s žádnými nebo nerozpoznanými informacemi o hodnocení:",
|
||||
"HeaderChapterImages": "Obrázky kapitol",
|
||||
@ -1066,7 +1066,7 @@
|
||||
"HeaderExternalIds": "Externí Id:",
|
||||
"HeaderFetcherSettings": "Nastavení načítání",
|
||||
"HeaderImageOptions": "Volby obrázku",
|
||||
"HeaderKodiMetadataHelp": "Chcete-li povolit nebo zakázat metadata v souborech NFO, upravte nastavení knihovny v sekci ukládání metadat.",
|
||||
"HeaderKodiMetadataHelp": "Chcete-li povolit nebo zakázat metadata v souborech NFO, upravte nastavení knihovny v sekci 'Ukládání metadat'.",
|
||||
"HeaderLiveTvTunerSetup": "Nastavení televizního tuneru",
|
||||
"HeaderNewDevices": "Nové zařízení",
|
||||
"HeaderPhotoAlbums": "Fotoalba",
|
||||
@ -1074,7 +1074,7 @@
|
||||
"HeaderSeriesStatus": "Stav seriálu",
|
||||
"HeaderStopRecording": "Zastavit nahrávání",
|
||||
"HeaderSubtitleDownloads": "Stahování titulků",
|
||||
"HeaderTypeImageFetchers": "Stahovače obrázků ({0})",
|
||||
"HeaderTypeImageFetchers": "Stahovače obrázků ({0}):",
|
||||
"HeaderVideoType": "Formát videa",
|
||||
"Horizontal": "Vodorovně",
|
||||
"HttpsRequiresCert": "Chcete-li povolit zabezpečená připojení, budete muset zadat důvěryhodný certifikát SSL, například Let's Encrypt. Zadejte prosím certifikát nebo zakažte zabezpečená připojení.",
|
||||
@ -1117,7 +1117,7 @@
|
||||
"LabelTypeMetadataDownloaders": "Stahovače metadat ({0}):",
|
||||
"LabelTypeText": "Text",
|
||||
"LabelUserAgent": "User agent:",
|
||||
"LabelUserRemoteClientBitrateLimitHelp": "Přepíše výchozí globální hodnotu nastavenou v nastavení přehrávání serveru.",
|
||||
"LabelUserRemoteClientBitrateLimitHelp": "Přepíše výchozí globální hodnotu nastavenou v nastavení serveru, viz Nástěnka > Přehrávání > Streamování.",
|
||||
"LabelVideoCodec": "Video kodek:",
|
||||
"LeaveBlankToNotSetAPassword": "Můžete ponechat prázdné pro nastavení bez hesla.",
|
||||
"LiveTV": "Televize",
|
||||
@ -1145,7 +1145,7 @@
|
||||
"Metadata": "Metadata",
|
||||
"MovieLibraryHelp": "Podívejte se na {0}průvodce pojmenováním filmů{1}.",
|
||||
"Never": "Nikdy",
|
||||
"NextUp": "Další",
|
||||
"NextUp": "Další díly",
|
||||
"NoNewDevicesFound": "Nebyla nalezena žádná nová zařízení. Chcete-li přidat nový tuner, zavřete tento dialog a zadejte informace o zařízení ručně.",
|
||||
"OnlyImageFormats": "Pouze obrazové formáty (VobSub, PGS, SUB, atd.)",
|
||||
"Option3D": "3D",
|
||||
@ -1158,7 +1158,7 @@
|
||||
"OptionLoginAttemptsBeforeLockout": "Počet chybných pokusů o přihlášení, který lze provést před zablokováním.",
|
||||
"OptionLoginAttemptsBeforeLockoutHelp": "0 znamená zdědění výchozí hodnoty 3 pokusů pro běžné uživatele a 5 pro administrátory. Nastavení na -1 deaktivuje funkci.",
|
||||
"OptionMax": "Max",
|
||||
"OptionProtocolHls": "Přímý přenos z internetu (HLS)",
|
||||
"OptionProtocolHls": "HTTP Live Streaming (HLS)",
|
||||
"OptionProtocolHttp": "HTTP",
|
||||
"OptionRequirePerfectSubtitleMatchHelp": "Vyžadování dokonalé shody filtruje titulky tak, aby obsahovaly pouze ty, které byly testovány a ověřeny s vaším přesným videosouborem. Zrušení zaškrtnutí tohoto políčka zvýší pravděpodobnost stahování titulků, ale zvýší pravděpodobnost chybného nebo nesprávného textu titulků.",
|
||||
"PasswordResetProviderHelp": "Zvolte poskytovatele resetování hesla, který bude použit při žádosti tohoto uživatele o resetování hesla.",
|
||||
@ -1186,13 +1186,13 @@
|
||||
"TabDirectPlay": "Přímé přehrávání",
|
||||
"TabServer": "Server",
|
||||
"TagsValue": "Tagy: {0}",
|
||||
"ThemeSongs": "Tematická hudba",
|
||||
"ThemeSongs": "Znělky",
|
||||
"ThemeVideos": "Tematická videa",
|
||||
"Trailers": "Upoutávky",
|
||||
"TvLibraryHelp": "Podívejte se na {0}průvodce pojmenováním TV pořadů{1}.",
|
||||
"Uniform": "Obrys",
|
||||
"Unplayed": "Nepřehrané",
|
||||
"UserAgentHelp": "Zadejte vlastní HTTP hlavičku user agenta.",
|
||||
"UserAgentHelp": "Zadejte vlastní HTTP hlavičku 'User-Agent'.",
|
||||
"ValueMinutes": "{0} min",
|
||||
"ValueOneAlbum": "1 album",
|
||||
"ValueOneSong": "1 skladba",
|
||||
@ -1368,8 +1368,8 @@
|
||||
"Data": "Datumy",
|
||||
"VideoAudio": "Video audio",
|
||||
"Photo": "Fotka",
|
||||
"LabelIconMaxResHelp": "Maximální rozlišení ikon daných vlastností upnp:icon.",
|
||||
"LabelAlbumArtMaxResHelp": "Maximální rozlišení obrázku v souboru dané vlastností upnp:albumArtURI.",
|
||||
"LabelIconMaxResHelp": "Maximální rozlišení ikon daných vlastností 'upnp:icon'.",
|
||||
"LabelAlbumArtMaxResHelp": "Maximální rozlišení obrázku v souboru dané vlastností 'upnp:albumArtURI'.",
|
||||
"Other": "Ostatní",
|
||||
"Bwdif": "BWDIF",
|
||||
"UseDoubleRateDeinterlacingHelp": "Toto nastavení při odstranění prokládání zdvojnásobuje snímkovou frekvenci, aby výsledné video vypadalo stejně plynule, jako při přehrávání prokládaného obsahu v televizi.",
|
||||
@ -1384,9 +1384,9 @@
|
||||
"LabelTonemappingDesat": "Snížení barevnosti při mapování tónů:",
|
||||
"TonemappingRangeHelp": "Výstupní rozsah barev. Automaticky znamená stejný jako vstupní.",
|
||||
"LabelTonemappingRange": "Rozsah mapování tónů:",
|
||||
"TonemappingAlgorithmHelp": "Mapování tonů je možné dále ladit. Pokud možnostem zde nerozumíte, je možné ponechat vše ve výchozím nastavení. Doporučená hodnota je Hable.",
|
||||
"TonemappingAlgorithmHelp": "Mapování tonů je možné dále ladit. Pokud možnostem zde nerozumíte, je možné ponechat vše ve výchozím nastavení. Doporučená hodnota je 'BT.2390'.",
|
||||
"LabelTonemappingAlgorithm": "Algoritmus mapování tónů:",
|
||||
"AllowTonemappingHelp": "Mapování tónů umožňuje změnit dynamický rozsah videa z HDR na SDR bez ztráty důležitých informací původního obrazu, např. detailů a barev. Tato funkce momentálně funguje pouze při překódování videí, které obsahují informace o HDR10 nebo HLG. Pokud je přehrávání trhané nebo vůbec nefunguje, zkuste vypnout příslušný hardwarový dekodér.",
|
||||
"AllowTonemappingHelp": "Mapování tónů umožňuje změnit dynamický rozsah videa z HDR na SDR bez ztráty detailů a barev, tj. důležitých informací původního obrazu. Tato funkce momentálně funguje pouze u videí, které obsahují HDR10 nebo HLG, a vyžaduje buď OpenCL nebo CUDA.",
|
||||
"EnableTonemapping": "Zapnout mapování tónů",
|
||||
"LabelOpenclDeviceHelp": "Zařízení OpenCL použité pro mapování tónů. Nalevo od tečky je číslo platformy, napravo pak číslo zařízení na této platformě. Výchozí hodnota je 0.0. Soubor aplikace FFmpeg, který obsahuje metodu pro hardwarovou akceleraci OpenCL, je povinný.",
|
||||
"LabelOpenclDevice": "Zařízení OpenCL:",
|
||||
@ -1401,7 +1401,7 @@
|
||||
"LabelMaxMuxingQueueSizeHelp": "Maximální počet paketů, které je možné napřed načíst při čekání na spuštění všech proudů. Pokud se stále zobrazuje chyba \"Příliš mnoho paketů načtených napřed ve výstupním proudu\" v protokolech FFmpeg, zkuste hodnotu zvýšit. Doporučená hodnota je 2048.",
|
||||
"LabelMaxMuxingQueueSize": "Maximální velikost muxovací fronty:",
|
||||
"LabelKnownProxies": "Známé proxy servery:",
|
||||
"KnownProxiesHelp": "Čárkami oddělený seznam IP adres nebo jmen hostitelů známých proxy serverů pro připojení k instanci Jellyfin. Vyžadováno pro správné využití HTTP hlavičky X-Forwarded-For. Vyžaduje restart.",
|
||||
"KnownProxiesHelp": "Čárkami oddělený seznam IP adres nebo jmen hostitelů známých proxy serverů pro připojení k instanci Jellyfin. Vyžadováno pro správné využití HTTP hlavičky 'X-Forwarded-For'. Vyžaduje restart.",
|
||||
"QuickConnectNotActive": "Rychlé připojení k tomuto serveru není povoleno",
|
||||
"QuickConnectNotAvailable": "Požádejte správce serveru, aby rychlé připojení povolil",
|
||||
"QuickConnectInvalidCode": "Neplatný kód pro rychlé připojení",
|
||||
@ -1502,7 +1502,7 @@
|
||||
"LabelMaxAudiobookResume": "Zbývající délka v minutách pro pokračování u audioknih:",
|
||||
"MessagePlaybackError": "Přehrání tohoto souboru na vašem zařízení Google Cast se nezdařilo.",
|
||||
"MessageChromecastConnectionError": "Vašemu zařízení Google Cast se nezdařilo kontaktovat server Jellyfin. Zkontrolujte připojení a zkuste to znovu.",
|
||||
"AllowVppTonemappingHelp": "Plně hardwarové mapování tónů bez použití filtru OpenCL. Momentálně funguje pouze při překódování videí, které obsahují metadata HDR10.",
|
||||
"AllowVppTonemappingHelp": "Plné mapování tónů pomocí ovladače Intel. Momentálně funguje pouze na určitém hardwaru u videí s HDR10. Má vyšší prioritu než jiné implementace OpenCL.",
|
||||
"EnableVppTonemapping": "Povolit mapování tónů VPP",
|
||||
"EnableEnhancedNvdecDecoder": "Povolit vylepšený dekodér NVDEC",
|
||||
"Framerate": "Snímková frekvence",
|
||||
@ -1517,10 +1517,10 @@
|
||||
"MessageSent": "Zpráva odeslána.",
|
||||
"LabelSlowResponseTime": "Čas v milisekundách, nad který je odezva považována za pomalou:",
|
||||
"LabelSlowResponseEnabled": "Zaznamenat varovnou zprávu, pokud byla odezva serveru pomalá",
|
||||
"UseEpisodeImagesInNextUpHelp": "Sekce Další a Pokračovat ve sledování použijí obrázky dílů jako náhledy místo primárního náhledu seriálů.",
|
||||
"UseEpisodeImagesInNextUpHelp": "Sekce 'Další' a 'Pokračovat ve sledování' použijí obrázky dílů jako náhledy místo primárního náhledu seriálů.",
|
||||
"UseEpisodeImagesInNextUp": "Použít obrázky dílů v sekcích 'Další' a 'Pokračovat ve sledování'",
|
||||
"LabelLocalCustomCss": "Úprava vzhledu prostřednictvím CSS, která se vztahuje pouze na tento klient. Možná bude potřeba vypnout CSS nabízené serverem.",
|
||||
"LabelDisableCustomCss": "Vypnout úpravy vzhledu prostřednictvím CSS nabízené serverem.",
|
||||
"LabelLocalCustomCss": "Úprava vzhledu prostřednictvím kódu CSS, který se vztahuje pouze na tento klient. Možná bude potřeba vypnout kód CSS nabízený serverem.",
|
||||
"LabelDisableCustomCss": "Vypnout úpravy vzhledu prostřednictvím kódu CSS nabízeného serverem.",
|
||||
"DisableCustomCss": "Vypnout CSS nabízené serverem",
|
||||
"AudioBitDepthNotSupported": "Bitová hloubka zvuku není podporována",
|
||||
"VideoProfileNotSupported": "Profil video kodeku není podporován",
|
||||
@ -1600,5 +1600,27 @@
|
||||
"TypeOptionPluralBoxSet": "Kolekce",
|
||||
"LabelAutomaticallyAddToCollectionHelp": "Pokud mají alespoň 2 filmy stejný název kolekce, budou automaticky přidány do kolekce.",
|
||||
"LabelAutomaticallyAddToCollection": "Automaticky přidat to kolekce",
|
||||
"Cursive": "Kurzíva"
|
||||
"Cursive": "Kurzíva",
|
||||
"LabelHardwareEncodingOptions": "Možnosti hardwarového kódování:",
|
||||
"IntelLowPowerEncHelp": "Nízkoenergetické kódování může zbytečně synchronizovat CPU s GPU. Na Linuxu musí být vypnuto, pokud není nakonfigurován firmware i915 HuC.",
|
||||
"EnableIntelLowPowerHevcHwEncoder": "Povolit nízkoenergetický hardwarový dekodér Intel HEVC",
|
||||
"EnableIntelLowPowerH264HwEncoder": "Povolit nízkoenergetický hardwarový dekodér Intel H.264",
|
||||
"PreferSystemNativeHwDecoder": "Preferovat hardwarové dekodéry DXVA nebo VA-API nativní pro daný OS",
|
||||
"ContainerBitrateExceedsLimit": "Bitový tok videa překračuje nastavenou mez",
|
||||
"DirectPlayError": "Při spuštění přímého přehrávání došlo k chybě",
|
||||
"UnknownAudioStreamInfo": "Informace o zvukové stopě jsou neznámé",
|
||||
"UnknownVideoStreamInfo": "Informace o video stopě jsou neznámé",
|
||||
"VideoBitrateNotSupported": "Bitový tok videa není podporovaný",
|
||||
"AudioIsExternal": "Zvuková stopa je externí",
|
||||
"SelectAll": "Vybrat vše",
|
||||
"ButtonExitApp": "Ukončit aplikaci",
|
||||
"ThemeVideo": "Úvodní video",
|
||||
"ThemeSong": "Znělka",
|
||||
"Sample": "Ukázka",
|
||||
"Scene": "Scéna",
|
||||
"Interview": "Rozhovor",
|
||||
"DeletedScene": "Vymazaná scéna",
|
||||
"BehindTheScenes": "Z natáčení",
|
||||
"Trailer": "Upoutávka",
|
||||
"Clip": "Krátký film"
|
||||
}
|
||||
|
@ -201,7 +201,7 @@
|
||||
"TypeOptionPluralSeries": "Rhaglenni teledu",
|
||||
"OptionTvdbRating": "Sgôr TheTVDB",
|
||||
"LiveTV": "Teledu Byw",
|
||||
"TV": "Teledu",
|
||||
"TV": "Set teledu",
|
||||
"LabelTitle": "Teitl:",
|
||||
"LabelTime": "Amser:",
|
||||
"LabelTheme": "Thema:",
|
||||
@ -447,5 +447,29 @@
|
||||
"ColorTransfer": "Trosglwyddo lliw",
|
||||
"ColorSpace": "Gofod lliw",
|
||||
"ColorPrimaries": "Sylfaenol lliwiau",
|
||||
"LabelAccessDay": "Diwrnod yr wythnos:"
|
||||
"LabelAccessDay": "Diwrnod yr wythnos:",
|
||||
"HeaderCancelSeries": "Canslo Cyfres",
|
||||
"HeaderCancelRecording": "Canslo Recordiad",
|
||||
"HeaderAutoDiscovery": "Canfod Rhwydwaith",
|
||||
"HeaderAudioSettings": "Gosodiadau Sain",
|
||||
"HeaderAudioBooks": "Llyfrau Sain",
|
||||
"HeaderAddUser": "Ychwanegu Defnyddiwr",
|
||||
"HeaderAdditionalParts": "Rhannau Ychwanegol",
|
||||
"HeaderActiveRecordings": "Gweithredol Recordiadau",
|
||||
"HeaderActiveDevices": "Gweithredol Dyfeisiau",
|
||||
"HeaderAccessSchedule": "Amserlen Mynediad",
|
||||
"GuideProviderSelectListings": "Dewis Rhestriadau",
|
||||
"GuestStar": "Seren wadd",
|
||||
"GroupVersions": "Fersiynau grŵp",
|
||||
"Fullscreen": "Sgrin lawn",
|
||||
"FormatValue": "Fformat: {0}",
|
||||
"ExtraLarge": "Mawr Ychwanegol",
|
||||
"EveryHour": "Bob awr",
|
||||
"Engineer": "Peiriannydd sain",
|
||||
"CancelSeries": "Canslo cyfres",
|
||||
"CancelRecording": "Canslo recordiad",
|
||||
"ButtonSelectDirectory": "Dewis Cyfeiriadur",
|
||||
"TypeOptionPluralAudio": "Synau",
|
||||
"OptionSpecialEpisode": "Penodau arbennig",
|
||||
"LabelProfileCodecs": "Codeciaid:"
|
||||
}
|
||||
|
@ -155,7 +155,7 @@
|
||||
"HeaderAddUpdateImage": "Tilføj/opdater billede",
|
||||
"HeaderAdditionalParts": "Andre stier",
|
||||
"HeaderAlert": "Advarsel",
|
||||
"HeaderAllowMediaDeletionFrom": "Tillad Media Sletning Fra",
|
||||
"HeaderAllowMediaDeletionFrom": "Tillad Media Sletning Fra:",
|
||||
"HeaderApiKey": "API-nøgle",
|
||||
"HeaderApiKeys": "API-nøgler",
|
||||
"HeaderApiKeysHelp": "Eksterne applikationer skal have en API-nøgle for at kunne kommunikere med serveren. Nøgler udstedes ved at logge ind med en normal bruger konto, eller ved manuelt at tildele applikationen en nøgle.",
|
||||
@ -197,7 +197,7 @@
|
||||
"HeaderEnabledFieldsHelp": "Fjern fluebenet fra et felt for at låse det og forhindre dets data fra at blive ændret.",
|
||||
"HeaderError": "Fejl",
|
||||
"HeaderExternalIds": "Eksterne ID'er:",
|
||||
"HeaderFeatureAccess": "Adgang til funktioner",
|
||||
"HeaderFeatureAccess": "Adgang til funktioner:",
|
||||
"HeaderFetchImages": "Hent billeder:",
|
||||
"HeaderFetcherSettings": "Henter indstillinger",
|
||||
"HeaderForKids": "For Børn",
|
||||
@ -242,7 +242,7 @@
|
||||
"HeaderPaths": "Stier",
|
||||
"HeaderPinCodeReset": "Nulstil pinkode",
|
||||
"HeaderPlayAll": "Afspil Alle",
|
||||
"HeaderPlayback": "Medieafspilning",
|
||||
"HeaderPlayback": "Medieafspilning:",
|
||||
"HeaderPlaybackError": "Fejl i afspilning",
|
||||
"HeaderPleaseSignIn": "Log venligst ind",
|
||||
"HeaderPluginInstallation": "Plugin installation",
|
||||
@ -252,7 +252,7 @@
|
||||
"HeaderRecentlyPlayed": "Afspillet for nyligt",
|
||||
"HeaderRecordingOptions": "Optagelsesindstillinger",
|
||||
"HeaderRecordingPostProcessing": "Efterbehandling af Optagelse",
|
||||
"HeaderRemoteControl": "Fjernbetjening",
|
||||
"HeaderRemoteControl": "Fjernbetjening:",
|
||||
"HeaderRemoveMediaFolder": "Fjern mediemappe",
|
||||
"HeaderRemoveMediaLocation": "Fjern medielokalisation",
|
||||
"HeaderResponseProfile": "Svarprofil",
|
||||
@ -288,7 +288,7 @@
|
||||
"HeaderTranscodingProfileHelp": "Tilføj profiler for transkodning foe at angive hvilke formater der skal anvendes når transkodning er nødvendig.",
|
||||
"HeaderTunerDevices": "Tuner-Enheder",
|
||||
"HeaderTuners": "Tunere",
|
||||
"HeaderTypeImageFetchers": "Billede Hentere ({0})",
|
||||
"HeaderTypeImageFetchers": "Billede Hentere ({0}):",
|
||||
"HeaderTypeText": "Indtast tekst",
|
||||
"HeaderUpcomingOnTV": "Kommende I TV",
|
||||
"HeaderUploadImage": "Upload Billede",
|
||||
@ -1528,5 +1528,8 @@
|
||||
"Engineer": "Ingeniør",
|
||||
"Conductor": "Dirigent",
|
||||
"Arranger": "Arrangør",
|
||||
"AgeValue": "({0} år gammel)"
|
||||
"AgeValue": "({0} år gammel)",
|
||||
"LabelAutomaticallyAddToCollection": "Automatisk tilføj til samling",
|
||||
"Cursive": "Kursiv",
|
||||
"Console": "Konsol"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user