mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' of https://github.com/jellyfin/jellyfin-web into jellyfin-master
This commit is contained in:
commit
8f55658c91
219 changed files with 21450 additions and 12796 deletions
|
@ -17,6 +17,10 @@ jobs:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||||
|
displayName: Set release version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
|
|
||||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
|
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
|
||||||
displayName: 'Build Dockerfile'
|
displayName: 'Build Dockerfile'
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||||
|
@ -119,4 +123,4 @@ jobs:
|
||||||
inputs:
|
inputs:
|
||||||
sshEndpoint: repository
|
sshEndpoint: repository
|
||||||
runOptions: 'inline'
|
runOptions: 'inline'
|
||||||
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'
|
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch)'
|
||||||
|
|
1
.copr
Symbolic link
1
.copr
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
fedora/
|
|
@ -1 +0,0 @@
|
||||||
../fedora/Makefile
|
|
101
.eslintrc.js
101
.eslintrc.js
|
@ -70,57 +70,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
'import/parsers': {
|
'import/parsers': {
|
||||||
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
|
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'./src/**/*.js',
|
|
||||||
'./src/**/*.ts'
|
|
||||||
],
|
|
||||||
parser: '@babel/eslint-parser',
|
|
||||||
env: {
|
|
||||||
node: false,
|
|
||||||
amd: true,
|
|
||||||
browser: true,
|
|
||||||
es6: true,
|
|
||||||
es2017: true,
|
|
||||||
es2020: true
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
// Browser globals
|
|
||||||
'MediaMetadata': 'readonly',
|
|
||||||
// Tizen globals
|
|
||||||
'tizen': 'readonly',
|
|
||||||
'webapis': 'readonly',
|
|
||||||
// WebOS globals
|
|
||||||
'webOS': 'readonly',
|
|
||||||
// Dependency globals
|
|
||||||
'$': 'readonly',
|
|
||||||
'jQuery': 'readonly',
|
|
||||||
// Jellyfin globals
|
|
||||||
'ApiClient': 'writable',
|
|
||||||
'chrome': 'writable',
|
|
||||||
'DlnaProfilePage': 'writable',
|
|
||||||
'DashboardPage': 'writable',
|
|
||||||
'Emby': 'readonly',
|
|
||||||
'getParameterByName': 'writable',
|
|
||||||
'getWindowLocationSearch': 'writable',
|
|
||||||
'Globalize': 'writable',
|
|
||||||
'Hls': 'writable',
|
|
||||||
'dfnshelper': 'writable',
|
|
||||||
'LibraryMenu': 'writable',
|
|
||||||
'LinkParser': 'writable',
|
|
||||||
'LiveTvHelpers': 'writable',
|
|
||||||
'Loading': 'writable',
|
|
||||||
'MetadataEditor': 'writable',
|
|
||||||
'PlaylistViewer': 'writable',
|
|
||||||
'UserParentalControlPage': 'writable',
|
|
||||||
'Windows': 'readonly'
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
polyfills: [
|
polyfills: [
|
||||||
// Native Promises Only
|
// Native Promises Only
|
||||||
'Promise',
|
'Promise',
|
||||||
|
@ -206,6 +156,57 @@ module.exports = {
|
||||||
// Temporary while eslint-compat-plugin is buggy
|
// Temporary while eslint-compat-plugin is buggy
|
||||||
'document.querySelector'
|
'document.querySelector'
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'./src/**/*.js',
|
||||||
|
'./src/**/*.ts'
|
||||||
|
],
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
env: {
|
||||||
|
node: false,
|
||||||
|
amd: true,
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
es2017: true,
|
||||||
|
es2020: true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
// Browser globals
|
||||||
|
'MediaMetadata': 'readonly',
|
||||||
|
// Tizen globals
|
||||||
|
'tizen': 'readonly',
|
||||||
|
'webapis': 'readonly',
|
||||||
|
// WebOS globals
|
||||||
|
'webOS': 'readonly',
|
||||||
|
// Dependency globals
|
||||||
|
'$': 'readonly',
|
||||||
|
'jQuery': 'readonly',
|
||||||
|
// Jellyfin globals
|
||||||
|
'ApiClient': 'writable',
|
||||||
|
'Events': 'writable',
|
||||||
|
'chrome': 'writable',
|
||||||
|
'DlnaProfilePage': 'writable',
|
||||||
|
'DashboardPage': 'writable',
|
||||||
|
'Emby': 'readonly',
|
||||||
|
'getParameterByName': 'writable',
|
||||||
|
'getWindowLocationSearch': 'writable',
|
||||||
|
'Globalize': 'writable',
|
||||||
|
'Hls': 'writable',
|
||||||
|
'dfnshelper': 'writable',
|
||||||
|
'LibraryMenu': 'writable',
|
||||||
|
'LinkParser': 'writable',
|
||||||
|
'LiveTvHelpers': 'writable',
|
||||||
|
'Loading': 'writable',
|
||||||
|
'MetadataEditor': 'writable',
|
||||||
|
'PlaylistViewer': 'writable',
|
||||||
|
'ServerNotifications': 'writable',
|
||||||
|
'TaskButton': 'writable',
|
||||||
|
'UserParentalControlPage': 'writable',
|
||||||
|
'Windows': 'readonly'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
3
.github/dependabot.yaml
vendored
3
.github/dependabot.yaml
vendored
|
@ -5,6 +5,9 @@ updates:
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
ignore:
|
||||||
|
- dependency-name: hls.js
|
||||||
|
update-types: [ version-update:semver-major ]
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
|
|
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@v2.4.0
|
uses: actions/setup-node@v2.5.1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 12
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -26,7 +26,7 @@ jobs:
|
||||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||||
|
@ -51,7 +51,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@v2.4.0
|
uses: actions/setup-node@v2.5.1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 12
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -61,7 +61,7 @@ jobs:
|
||||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||||
|
@ -89,7 +89,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@v2.4.0
|
uses: actions/setup-node@v2.5.1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 12
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -99,7 +99,7 @@ jobs:
|
||||||
run: echo "::set-output name=dir::$(npm config get cache)"
|
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -12,3 +12,10 @@ config.json
|
||||||
|
|
||||||
# log
|
# log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
# vim
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# build artifacts
|
||||||
|
fedora/jellyfin-web-*.src.rpm
|
||||||
|
fedora/jellyfin-web-*.tar.gz
|
||||||
|
|
|
@ -59,7 +59,6 @@
|
||||||
"declaration-colon-space-after": "always-single-line",
|
"declaration-colon-space-after": "always-single-line",
|
||||||
"declaration-colon-space-before": "never",
|
"declaration-colon-space-before": "never",
|
||||||
"font-family-no-duplicate-names": true,
|
"font-family-no-duplicate-names": true,
|
||||||
"function-calc-no-invalid": true,
|
|
||||||
"function-calc-no-unspaced-operator": true,
|
"function-calc-no-unspaced-operator": true,
|
||||||
"function-comma-newline-after": "always-multi-line",
|
"function-comma-newline-after": "always-multi-line",
|
||||||
"function-comma-space-after": "always-single-line",
|
"function-comma-space-after": "always-single-line",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": [ "./.stylelintrc.json" ],
|
"extends": [ "./.stylelintrc.json" ],
|
||||||
|
"customSyntax": "postcss-scss",
|
||||||
"plugins": [ "stylelint-scss" ],
|
"plugins": [ "stylelint-scss" ],
|
||||||
"rules": {
|
"rules": {
|
||||||
"at-rule-no-unknown": null,
|
"at-rule-no-unknown": null,
|
||||||
|
|
|
@ -45,9 +45,11 @@
|
||||||
- [Camc314](https://github.com/camc314)
|
- [Camc314](https://github.com/camc314)
|
||||||
- [danieladov](https://github.com/danieladov)
|
- [danieladov](https://github.com/danieladov)
|
||||||
- [Stephane Senart](https://github.com/ssenart)
|
- [Stephane Senart](https://github.com/ssenart)
|
||||||
|
- [imchasingshadows](https://github.com/imchasingshadows)
|
||||||
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
|
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
|
||||||
- [Keegan Dahm](https://github.com/keegandahm)
|
- [Keegan Dahm](https://github.com/keegandahm)
|
||||||
- [GodTamIt](https://github.com/GodTamIt)
|
- [GodTamIt](https://github.com/GodTamIt)
|
||||||
|
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
@ -111,3 +113,4 @@
|
||||||
- [tikuf](https://github.com/tikuf/)
|
- [tikuf](https://github.com/tikuf/)
|
||||||
- [Tim Hobbs](https://github.com/timhobbs)
|
- [Tim Hobbs](https://github.com/timhobbs)
|
||||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||||
|
- [jomp16](https://github.com/jomp16)
|
||||||
|
|
2
build.sh
2
build.sh
|
@ -39,7 +39,7 @@ do_build_native() {
|
||||||
}
|
}
|
||||||
|
|
||||||
do_build_docker() {
|
do_build_docker() {
|
||||||
if ! dpkg --print-architecture | grep -q 'amd64'; then
|
if ! [ $(uname -m) = "x86_64" ]; then
|
||||||
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
|
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
37
bump_version
37
bump_version
|
@ -18,38 +18,39 @@ if [[ -z $1 ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
shared_version_file="src/components/apphost.js"
|
|
||||||
build_file="./build.yaml"
|
build_file="./build.yaml"
|
||||||
|
package_file="./package*.json"
|
||||||
|
|
||||||
new_version="$1"
|
new_version="$1"
|
||||||
|
|
||||||
# Parse the version from shared version file
|
old_version="$(
|
||||||
old_version="$( grep "appVersion" ${shared_version_file} | head -1 | sed -E "s/var appVersion = '([0-9\.]+)';/\1/" | tr -d '[:space:]' )"
|
grep "version:" ${build_file} \
|
||||||
echo "Old version in appHost is: $old_version"
|
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
||||||
|
)"
|
||||||
|
echo "Old version: ${old_version}"
|
||||||
|
|
||||||
# Set the shared version to the specified new_version
|
# Bump the NPM version
|
||||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
|
||||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
|
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
||||||
|
|
||||||
old_version="$( grep "version:" ${build_file} | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/' )"
|
|
||||||
echo "Old version in ${build_file}: ${old_version}"
|
|
||||||
|
|
||||||
# Set the build.yaml version to the specified new_version
|
# Set the build.yaml version to the specified new_version
|
||||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||||
sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
|
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
|
||||||
|
|
||||||
|
|
||||||
if [[ ${new_version} == *"-"* ]]; then
|
if [[ ${new_version} == *"-"* ]]; then
|
||||||
new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
|
new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
|
||||||
|
new_version_deb_sup=""
|
||||||
else
|
else
|
||||||
new_version_deb="${new_version}-1"
|
new_version_pkg="${new_version}"
|
||||||
|
new_version_deb_sup="-1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
||||||
debian_changelog_file="debian/changelog"
|
debian_changelog_file="debian/changelog"
|
||||||
debian_changelog_temp="$( mktemp )"
|
debian_changelog_temp="$( mktemp )"
|
||||||
# Create new temp file with our changelog
|
# Create new temp file with our changelog
|
||||||
echo -e "jellyfin-web (${new_version_deb}) unstable; urgency=medium
|
echo -e "jellyfin-web (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
|
||||||
|
|
||||||
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}
|
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ pushd ${fedora_spec_temp_dir}
|
||||||
# Split out the stuff before and after changelog
|
# Split out the stuff before and after changelog
|
||||||
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
|
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
|
||||||
# Update the version in xx00
|
# Update the version in xx00
|
||||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
|
sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
|
||||||
# Remove the header from xx01
|
# Remove the header from xx01
|
||||||
sed -i '/^%changelog/d' xx01
|
sed -i '/^%changelog/d' xx01
|
||||||
# Create new temp file with our changelog
|
# Create new temp file with our changelog
|
||||||
|
@ -84,8 +85,8 @@ popd
|
||||||
# Move into place
|
# Move into place
|
||||||
mv ${fedora_spec_temp} ${fedora_spec_file}
|
mv ${fedora_spec_temp} ${fedora_spec_file}
|
||||||
# Clean up
|
# Clean up
|
||||||
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
|
rm -rf ${fedora_spec_temp_dir}
|
||||||
|
|
||||||
# Stage the changed files for commit
|
# Stage the changed files for commit
|
||||||
git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file}
|
git add .
|
||||||
git status
|
git status -v
|
||||||
|
|
|
@ -1,21 +1,44 @@
|
||||||
VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin-web.spec)
|
DIR := $(dir $(lastword $(MAKEFILE_LIST)))
|
||||||
|
# install git and npm
|
||||||
|
$(info $(shell set -x; if [ "$$(id -u)" = "0" ]; then echo "Installing git"; dnf -y install git npm; fi))
|
||||||
|
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)
|
||||||
|
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
||||||
|
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
||||||
|
|
||||||
srpm:
|
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/
|
||||||
cd fedora/; \
|
|
||||||
|
outdir ?= $(PWD)/$(DIR)/
|
||||||
|
TARGET ?= fedora-35-x86_64
|
||||||
|
|
||||||
|
srpm: $(DIR)/$(SRPM)
|
||||||
|
tarball: $(DIR)/$(TARBALL)
|
||||||
|
|
||||||
|
$(DIR)/$(TARBALL):
|
||||||
|
cd $(DIR)/; \
|
||||||
SOURCE_DIR=.. \
|
SOURCE_DIR=.. \
|
||||||
WORKDIR="$${PWD}"; \
|
WORKDIR="$${PWD}"; \
|
||||||
|
version=$(VERSION); \
|
||||||
tar \
|
tar \
|
||||||
--transform "s,^\.,jellyfin-web-$(VERSION)," \
|
--transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
|
||||||
--exclude='.git*' \
|
--exclude='.git*' \
|
||||||
--exclude='**/.git' \
|
--exclude='**/.git' \
|
||||||
--exclude='**/.hg' \
|
--exclude='**/.hg' \
|
||||||
--exclude='deployment' \
|
--exclude=deployment \
|
||||||
--exclude='*.deb' \
|
--exclude='*.deb' \
|
||||||
--exclude='*.rpm' \
|
--exclude='*.rpm' \
|
||||||
--exclude='jellyfin-web-$(VERSION).tar.gz' \
|
--exclude=$(notdir $@) \
|
||||||
-czf "jellyfin-web-$(VERSION).tar.gz" \
|
-czf $(notdir $@) \
|
||||||
-C $${SOURCE_DIR} ./
|
-C $${SOURCE_DIR} ./
|
||||||
cd fedora/; \
|
|
||||||
rpmbuild -bs jellyfin-web.spec \
|
$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin-web.spec
|
||||||
|
cd $(DIR)/; \
|
||||||
|
rpmbuild -bs $(NAME).spec \
|
||||||
--define "_sourcedir $$PWD/" \
|
--define "_sourcedir $$PWD/" \
|
||||||
--define "_srcrpmdir $(outdir)"
|
--define "_srcrpmdir $(outdir)"
|
||||||
|
|
||||||
|
rpms: $(DIR)/$(SRPM)
|
||||||
|
mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
|
||||||
|
--enable-network \
|
||||||
|
-r $(TARGET) $<
|
||||||
|
|
|
@ -10,8 +10,11 @@ URL: https://jellyfin.org
|
||||||
Source0: jellyfin-web-%{version}.tar.gz
|
Source0: jellyfin-web-%{version}.tar.gz
|
||||||
|
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
%if 0%{?fedora} >= 33
|
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
||||||
BuildRequires: nodejs
|
BuildRequires: nodejs
|
||||||
|
%else
|
||||||
|
BuildRequires: git
|
||||||
|
BuildRequires: npm
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
# Disable Automatic Dependency Processing
|
# Disable Automatic Dependency Processing
|
||||||
|
@ -27,7 +30,10 @@ Jellyfin is a free software media system that puts you in control of managing an
|
||||||
%build
|
%build
|
||||||
|
|
||||||
%install
|
%install
|
||||||
|
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
||||||
|
# Required for CentOS build
|
||||||
chown root:root -R .
|
chown root:root -R .
|
||||||
|
%endif
|
||||||
npm ci --no-audit --unsafe-perm
|
npm ci --no-audit --unsafe-perm
|
||||||
%{__mkdir} -p %{buildroot}%{_datadir}
|
%{__mkdir} -p %{buildroot}%{_datadir}
|
||||||
mv dist %{buildroot}%{_datadir}/jellyfin-web
|
mv dist %{buildroot}%{_datadir}/jellyfin-web
|
||||||
|
|
8428
package-lock.json
generated
8428
package-lock.json
generated
File diff suppressed because it is too large
Load diff
120
package.json
120
package.json
|
@ -5,92 +5,92 @@
|
||||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.15.5",
|
"@babel/core": "^7.16.7",
|
||||||
"@babel/eslint-parser": "^7.15.4",
|
"@babel/eslint-parser": "^7.16.5",
|
||||||
"@babel/eslint-plugin": "^7.14.5",
|
"@babel/eslint-plugin": "^7.16.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
"@babel/plugin-proposal-private-methods": "^7.14.5",
|
"@babel/plugin-proposal-private-methods": "^7.16.7",
|
||||||
"@babel/plugin-transform-modules-umd": "^7.14.5",
|
"@babel/plugin-transform-modules-umd": "^7.16.7",
|
||||||
"@babel/preset-env": "^7.15.4",
|
"@babel/preset-env": "^7.16.7",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"@babel/preset-typescript": "^7.15.0",
|
"@babel/preset-typescript": "^7.16.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
"@uupaa/dynamic-import-polyfill": "^1.0.2",
|
"@uupaa/dynamic-import-polyfill": "^1.0.2",
|
||||||
"autoprefixer": "^10.3.3",
|
"autoprefixer": "^10.4.1",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-dynamic-import-polyfill": "^1.0.0",
|
"babel-plugin-dynamic-import-polyfill": "^1.0.0",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"confusing-browser-globals": "^1.0.10",
|
"confusing-browser-globals": "^1.0.11",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"copy-webpack-plugin": "^10.2.0",
|
||||||
"css-loader": "^5.2.6",
|
"css-loader": "^6.5.1",
|
||||||
"cssnano": "^5.0.8",
|
"cssnano": "^5.0.14",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-compat": "^3.13.0",
|
"eslint-plugin-compat": "^4.0.0",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-import": "^2.24.2",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-promise": "^5.1.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-react": "^7.25.1",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"expose-loader": "^3.0.0",
|
"expose-loader": "^3.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"html-loader": "^3.0.1",
|
||||||
"html-loader": "^2.1.2",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"html-webpack-plugin": "^5.3.2",
|
"postcss": "^8.4.5",
|
||||||
"postcss": "^8.3.6",
|
"postcss-loader": "^6.2.1",
|
||||||
"postcss-loader": "^6.1.1",
|
"postcss-preset-env": "^7.2.0",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-scss": "^4.0.2",
|
||||||
"sass": "^1.39.0",
|
"sass": "^1.45.2",
|
||||||
"sass-loader": "^12.1.0",
|
"sass-loader": "^12.4.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.1",
|
||||||
"style-loader": "^3.2.1",
|
"style-loader": "^3.3.1",
|
||||||
"stylelint": "^13.13.1",
|
"stylelint": "^14.2.0",
|
||||||
"stylelint-config-rational-order": "^0.1.2",
|
"stylelint-config-rational-order": "^0.1.2",
|
||||||
"stylelint-no-browser-hacks": "^1.2.1",
|
"stylelint-no-browser-hacks": "^1.2.1",
|
||||||
"stylelint-order": "^4.1.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"stylelint-scss": "^3.20.1",
|
"stylelint-scss": "^4.1.0",
|
||||||
"ts-loader": "^9.2.5",
|
"ts-loader": "^9.2.6",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.5.4",
|
||||||
"webpack": "^5.52.0",
|
"webpack": "^5.65.0",
|
||||||
"webpack-cli": "^4.8.0",
|
"webpack-cli": "^4.9.1",
|
||||||
"webpack-dev-server": "^4.1.0",
|
"webpack-dev-server": "^4.7.2",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"workbox-webpack-plugin": "^6.2.4",
|
"workbox-webpack-plugin": "^6.2.4",
|
||||||
"worker-plugin": "^5.0.1"
|
"worker-loader": "^3.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/noto-sans": "^4.5.0",
|
"@fontsource/noto-sans": "^4.5.1",
|
||||||
"@fontsource/noto-sans-hk": "^4.5.0",
|
"@fontsource/noto-sans-hk": "^4.5.2",
|
||||||
"@fontsource/noto-sans-jp": "^4.5.0",
|
"@fontsource/noto-sans-jp": "^4.5.2",
|
||||||
"@fontsource/noto-sans-kr": "^4.5.0",
|
"@fontsource/noto-sans-kr": "^4.5.2",
|
||||||
"@fontsource/noto-sans-sc": "^4.5.0",
|
"@fontsource/noto-sans-sc": "^4.5.2",
|
||||||
"blurhash": "^1.1.4",
|
"blurhash": "^1.1.4",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"core-js": "^3.17.2",
|
"core-js": "^3.20.2",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.28.0",
|
||||||
"dompurify": "^2.3.1",
|
"dompurify": "^2.3.4",
|
||||||
"epubjs": "^0.3.85",
|
"epubjs": "^0.3.90",
|
||||||
"fast-text-encoding": "^1.0.3",
|
"fast-text-encoding": "^1.0.3",
|
||||||
"flv.js": "^1.6.1",
|
"flv.js": "^1.6.2",
|
||||||
"headroom.js": "^0.12.0",
|
"headroom.js": "^0.12.0",
|
||||||
"hls.js": "^1.0.10",
|
"hls.js": "^0.14.17",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jellyfin-apiclient": "^1.8.0",
|
"jellyfin-apiclient": "^1.10.0",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"jstree": "^3.3.12",
|
"jstree": "^3.3.12",
|
||||||
"libarchive.js": "^1.3.0",
|
"libarchive.js": "^1.3.0",
|
||||||
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-smarttv",
|
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^3.0.2",
|
"marked": "^4.0.8",
|
||||||
"material-design-icons-iconfont": "^6.1.0",
|
"material-design-icons-iconfont": "^6.1.1",
|
||||||
"native-promise-only": "^0.8.0-a",
|
"native-promise-only": "^0.8.0-a",
|
||||||
"page": "^1.11.6",
|
"page": "^1.11.6",
|
||||||
"pdfjs-dist": "2.6.347",
|
"pdfjs-dist": "2.12.313",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"screenfull": "^5.1.0",
|
"screenfull": "^6.0.0",
|
||||||
"sortablejs": "^1.14.0",
|
"sortablejs": "^1.14.0",
|
||||||
"swiper": "^6.8.4",
|
"swiper": "^6.8.4",
|
||||||
"webcomponents.js": "^0.7.24",
|
"webcomponents.js": "^0.7.24",
|
||||||
|
|
|
@ -1,3 +1,37 @@
|
||||||
|
// The padding of the header content on mobile needs to be adjusted
|
||||||
|
// based on the size of the poster card (values from card.scss)
|
||||||
|
@mixin header-poster-padding() {
|
||||||
|
padding-left: 37.5%;
|
||||||
|
|
||||||
|
@media all and (min-width: 43.75em) {
|
||||||
|
padding-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 50em) {
|
||||||
|
padding-left: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 75em) {
|
||||||
|
padding-left: 16.666666666666666666666666666667%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 87.5em) {
|
||||||
|
padding-left: 14.285714285714285714285714285714%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 100em) {
|
||||||
|
padding-left: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 120em) {
|
||||||
|
padding-left: 11.111111111111111111111111111111%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 131.25em) {
|
||||||
|
padding-left: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.headerUserImage,
|
.headerUserImage,
|
||||||
.navMenuOption,
|
.navMenuOption,
|
||||||
.pageTitle {
|
.pageTitle {
|
||||||
|
@ -580,11 +614,14 @@
|
||||||
.layout-mobile .mainDetailButtons {
|
.layout-mobile .mainDetailButtons {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
margin-left: 37.5%;
|
margin-left: 0;
|
||||||
|
|
||||||
|
@include header-poster-padding;
|
||||||
|
|
||||||
|
// The buttons row is full width on small screens
|
||||||
@media all and (max-width: 32em) {
|
@media all and (max-width: 32em) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,7 +672,7 @@
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
|
|
||||||
.layout-mobile & {
|
.layout-mobile & {
|
||||||
padding-left: 37.5%;
|
@include header-poster-padding;
|
||||||
|
|
||||||
@media all and (max-width: 32em) {
|
@media all and (max-width: 32em) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
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 |
|
@ -1,4 +1,4 @@
|
||||||
|
import { appRouter } from './appRouter';
|
||||||
import browser from '../scripts/browser';
|
import browser from '../scripts/browser';
|
||||||
import dialog from './dialog/dialog';
|
import dialog from './dialog/dialog';
|
||||||
import globalize from '../scripts/globalize';
|
import globalize from '../scripts/globalize';
|
||||||
|
@ -10,7 +10,16 @@ import globalize from '../scripts/globalize';
|
||||||
return originalString.replace(reg, strWith);
|
return originalString.replace(reg, strWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (text, title) {
|
function useNativeAlert() {
|
||||||
|
// webOS seems to block modals
|
||||||
|
// Tizen 2.x seems to block modals
|
||||||
|
return !browser.web0s
|
||||||
|
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
||||||
|
&& browser.tv
|
||||||
|
&& window.alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (text, title) {
|
||||||
let options;
|
let options;
|
||||||
if (typeof text === 'string') {
|
if (typeof text === 'string') {
|
||||||
options = {
|
options = {
|
||||||
|
@ -21,8 +30,11 @@ import globalize from '../scripts/globalize';
|
||||||
options = text;
|
options = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browser.tv && window.alert) {
|
await appRouter.ready();
|
||||||
|
|
||||||
|
if (useNativeAlert()) {
|
||||||
alert(replaceAll(options.text || '', '<br/>', '\n'));
|
alert(replaceAll(options.text || '', '<br/>', '\n'));
|
||||||
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
|
@ -35,8 +47,6 @@ import globalize from '../scripts/globalize';
|
||||||
options.buttons = items;
|
options.buttons = items;
|
||||||
return dialog.show(options);
|
return dialog.show(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable indent */
|
/* eslint-enable indent */
|
||||||
|
|
|
@ -280,6 +280,16 @@ import 'material-design-icons-iconfont';
|
||||||
element.removeEventListener(name, fn);
|
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) {
|
visible(visible) {
|
||||||
const element = this.options.element;
|
const element = this.options.element;
|
||||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||||
|
|
|
@ -24,6 +24,7 @@ class AppRouter {
|
||||||
isDummyBackToHome;
|
isDummyBackToHome;
|
||||||
msgTimeout;
|
msgTimeout;
|
||||||
popstateOccurred = false;
|
popstateOccurred = false;
|
||||||
|
promiseShow;
|
||||||
resolveOnNextShow;
|
resolveOnNextShow;
|
||||||
previousRoute = {};
|
previousRoute = {};
|
||||||
/**
|
/**
|
||||||
|
@ -44,13 +45,7 @@ class AppRouter {
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('viewshow', () => {
|
document.addEventListener('viewshow', () => this.onViewShow());
|
||||||
const resolve = this.resolveOnNextShow;
|
|
||||||
if (resolve) {
|
|
||||||
this.resolveOnNextShow = null;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), '');
|
this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), '');
|
||||||
// support hashbang
|
// support hashbang
|
||||||
|
@ -128,11 +123,24 @@ class AppRouter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
ready() {
|
||||||
page.back();
|
return this.promiseShow || Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
show(path, options) {
|
async back() {
|
||||||
|
if (this.promiseShow) await this.promiseShow;
|
||||||
|
|
||||||
|
this.promiseShow = new Promise((resolve) => {
|
||||||
|
this.resolveOnNextShow = resolve;
|
||||||
|
page.back();
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.promiseShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
async show(path, options) {
|
||||||
|
if (this.promiseShow) await this.promiseShow;
|
||||||
|
|
||||||
// ensure the path does not start with '#!' since the router adds this
|
// ensure the path does not start with '#!' since the router adds this
|
||||||
if (path.startsWith('#!')) {
|
if (path.startsWith('#!')) {
|
||||||
path = path.substring(2);
|
path = path.substring(2);
|
||||||
|
@ -152,17 +160,25 @@ class AppRouter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
this.promiseShow = new Promise((resolve) => {
|
||||||
this.resolveOnNextShow = resolve;
|
this.resolveOnNextShow = resolve;
|
||||||
page.show(path, options);
|
// Schedule a call to return the promise
|
||||||
|
setTimeout(() => page.show(path, options), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.promiseShow;
|
||||||
}
|
}
|
||||||
|
|
||||||
showDirect(path) {
|
async showDirect(path) {
|
||||||
return new Promise(function(resolve) {
|
if (this.promiseShow) await this.promiseShow;
|
||||||
|
|
||||||
|
this.promiseShow = new Promise((resolve) => {
|
||||||
this.resolveOnNextShow = resolve;
|
this.resolveOnNextShow = resolve;
|
||||||
page.show(this.baseUrl() + path);
|
// Schedule a call to return the promise
|
||||||
|
setTimeout(() => page.show(this.baseUrl() + path), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.promiseShow;
|
||||||
}
|
}
|
||||||
|
|
||||||
start(options) {
|
start(options) {
|
||||||
|
@ -417,6 +433,15 @@ class AppRouter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onViewShow() {
|
||||||
|
const resolve = this.resolveOnNextShow;
|
||||||
|
if (resolve) {
|
||||||
|
this.promiseShow = null;
|
||||||
|
this.resolveOnNextShow = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onForcedLogoutMessageTimeout() {
|
onForcedLogoutMessageTimeout() {
|
||||||
const msg = this.forcedLogoutMsg;
|
const msg = this.forcedLogoutMsg;
|
||||||
this.forcedLogoutMsg = null;
|
this.forcedLogoutMsg = null;
|
||||||
|
@ -638,7 +663,11 @@ class AppRouter {
|
||||||
|
|
||||||
const ignore = route.dummyRoute === true || this.previousRoute.dummyRoute === true;
|
const ignore = route.dummyRoute === true || this.previousRoute.dummyRoute === true;
|
||||||
this.previousRoute = route;
|
this.previousRoute = route;
|
||||||
if (ignore) return;
|
if (ignore) {
|
||||||
|
// Resolve 'show' promise
|
||||||
|
this.onViewShow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.handleRoute(ctx, next, route);
|
this.handleRoute(ctx, next, route);
|
||||||
};
|
};
|
||||||
|
@ -768,6 +797,10 @@ class AppRouter {
|
||||||
return '#!/list.html?type=Programs&IsAiring=true&serverId=' + options.serverId;
|
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') {
|
if (options.section === 'dvrschedule') {
|
||||||
return '#!/livetv.html?tab=4&serverId=' + options.serverId;
|
return '#!/livetv.html?tab=4&serverId=' + options.serverId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,11 +150,14 @@ button::-moz-focus-inner {
|
||||||
left: 0.3em;
|
left: 0.3em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 1.6em;
|
font-size: 88%;
|
||||||
height: 1.6em;
|
font-weight: 500;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: rgb(51, 136, 204);
|
background: rgb(51, 136, 204);
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardImageContainer {
|
.cardImageContainer {
|
||||||
|
@ -330,6 +333,7 @@ button::-moz-focus-inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.innerCardFooter > .cardText {
|
.innerCardFooter > .cardText {
|
||||||
|
@ -352,7 +356,8 @@ button::-moz-focus-inner {
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardTextCentered {
|
.cardTextCentered,
|
||||||
|
.cardTextCentered > .textActionButton {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -771,6 +771,7 @@ import ServerConnections from '../ServerConnections';
|
||||||
* @returns {string} HTML markup of the card's footer text element.
|
* @returns {string} HTML markup of the card's footer text element.
|
||||||
*/
|
*/
|
||||||
function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) {
|
function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) {
|
||||||
|
item = item.ProgramInfo || item;
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (logoUrl) {
|
if (logoUrl) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { appRouter } from '../appRouter';
|
||||||
import browser from '../../scripts/browser';
|
import browser from '../../scripts/browser';
|
||||||
import dialog from '../dialog/dialog';
|
import dialog from '../dialog/dialog';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
@ -6,7 +7,16 @@ function replaceAll(str, find, replace) {
|
||||||
return str.split(find).join(replace);
|
return str.split(find).join(replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeConfirm(options) {
|
function useNativeConfirm() {
|
||||||
|
// webOS seems to block modals
|
||||||
|
// Tizen 2.x seems to block modals
|
||||||
|
return !browser.web0s
|
||||||
|
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
||||||
|
&& browser.tv
|
||||||
|
&& window.confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nativeConfirm(options) {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = {
|
options = {
|
||||||
title: '',
|
title: '',
|
||||||
|
@ -15,6 +25,7 @@ function nativeConfirm(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = replaceAll(options.text || '', '<br/>', '\n');
|
const text = replaceAll(options.text || '', '<br/>', '\n');
|
||||||
|
await appRouter.ready();
|
||||||
const result = window.confirm(text);
|
const result = window.confirm(text);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
@ -24,7 +35,7 @@ function nativeConfirm(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function customConfirm(text, title) {
|
async function customConfirm(text, title) {
|
||||||
let options;
|
let options;
|
||||||
if (typeof text === 'string') {
|
if (typeof text === 'string') {
|
||||||
options = {
|
options = {
|
||||||
|
@ -51,6 +62,8 @@ function customConfirm(text, title) {
|
||||||
|
|
||||||
options.buttons = items;
|
options.buttons = items;
|
||||||
|
|
||||||
|
await appRouter.ready();
|
||||||
|
|
||||||
return dialog.show(options).then(result => {
|
return dialog.show(options).then(result => {
|
||||||
if (result === 'ok') {
|
if (result === 'ok') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -60,6 +73,6 @@ function customConfirm(text, title) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirm = browser.tv && window.confirm ? nativeConfirm : customConfirm;
|
const confirm = useNativeConfirm() ? nativeConfirm : customConfirm;
|
||||||
|
|
||||||
export default confirm;
|
export default confirm;
|
||||||
|
|
32
src/components/dashboard/users/ButtonElement.tsx
Normal file
32
src/components/dashboard/users/ButtonElement.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createButtonElement = ({ type, className, title }) => ({
|
||||||
|
__html: `<button
|
||||||
|
is="emby-button"
|
||||||
|
type="${type}"
|
||||||
|
class="${className}"
|
||||||
|
>
|
||||||
|
<span>${title}</span>
|
||||||
|
</button>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
type?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createButtonElement({
|
||||||
|
type: type,
|
||||||
|
className: className,
|
||||||
|
title: globalize.translate(title)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ButtonElement;
|
36
src/components/dashboard/users/CheckBoxElement.tsx
Normal file
36
src/components/dashboard/users/CheckBoxElement.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({
|
||||||
|
__html: `<label class="${labelClassName}">
|
||||||
|
<input
|
||||||
|
is="emby-checkbox"
|
||||||
|
type="${type}"
|
||||||
|
class="${className}"
|
||||||
|
/>
|
||||||
|
<span>${title}</span>
|
||||||
|
</label>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
labelClassName?: string;
|
||||||
|
type?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckBoxElement;
|
39
src/components/dashboard/users/CheckBoxListItem.tsx
Normal file
39
src/components/dashboard/users/CheckBoxListItem.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
className?: string;
|
||||||
|
Name?: string;
|
||||||
|
Id?: string;
|
||||||
|
AppName?: string;
|
||||||
|
checkedAttribute?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCheckBoxElement = ({className, Name, Id, AppName, checkedAttribute}) => ({
|
||||||
|
__html: `<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
is="emby-checkbox"
|
||||||
|
class="${className}"
|
||||||
|
data-id="${Id}" ${checkedAttribute}
|
||||||
|
/>
|
||||||
|
<span>${Name} ${AppName}</span>
|
||||||
|
</label>`
|
||||||
|
});
|
||||||
|
|
||||||
|
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, AppName, checkedAttribute}: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='sectioncheckbox'
|
||||||
|
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||||
|
className: className,
|
||||||
|
Name: Name,
|
||||||
|
Id: Id,
|
||||||
|
AppName: AppName ? `- ${AppName}` : '',
|
||||||
|
checkedAttribute: checkedAttribute
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckBoxListItem;
|
||||||
|
|
34
src/components/dashboard/users/InputElement.tsx
Normal file
34
src/components/dashboard/users/InputElement.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createInputElement = ({ type, id, label, options }) => ({
|
||||||
|
__html: `<input
|
||||||
|
is="emby-input"
|
||||||
|
type="${type}"
|
||||||
|
id="${id}"
|
||||||
|
label="${label}"
|
||||||
|
${options}
|
||||||
|
/>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
type?: string;
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
options?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputElement: FunctionComponent<IProps> = ({ type, id, label, options }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createInputElement({
|
||||||
|
type: type,
|
||||||
|
id: id,
|
||||||
|
label: globalize.translate(label),
|
||||||
|
options: options ? options : ''
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputElement;
|
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;
|
33
src/components/dashboard/users/SectionTitleButtonElement.tsx
Normal file
33
src/components/dashboard/users/SectionTitleButtonElement.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createButtonElement = ({ className, title, icon }) => ({
|
||||||
|
__html: `<button
|
||||||
|
is="emby-button"
|
||||||
|
type="button"
|
||||||
|
class="${className}"
|
||||||
|
style="margin-left:1em;"
|
||||||
|
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
|
||||||
|
dangerouslySetInnerHTML={createButtonElement({
|
||||||
|
className: className,
|
||||||
|
title: globalize.translate(title),
|
||||||
|
icon: icon
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionTitleButtonElement;
|
34
src/components/dashboard/users/SectionTitleLinkElement.tsx
Normal file
34
src/components/dashboard/users/SectionTitleLinkElement.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createLinkElement = ({ className, title, href }) => ({
|
||||||
|
__html: `<a
|
||||||
|
is="emby-linkbutton"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="${className}"
|
||||||
|
target="_blank"
|
||||||
|
href="${href}"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</a>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionTitleLinkElement: FunctionComponent<IProps> = ({ className, title, url }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createLinkElement({
|
||||||
|
className: className,
|
||||||
|
title: globalize.translate(title),
|
||||||
|
href: url
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionTitleLinkElement;
|
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;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
|
||||||
|
const createSelectElement = ({ className, id, label }) => ({
|
||||||
|
__html: `<select
|
||||||
|
className="${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;
|
100
src/components/dashboard/users/UserCardBox.tsx
Normal file
100
src/components/dashboard/users/UserCardBox.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { localeWithSuffix } from '../../../scripts/dfnshelper';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
import cardBuilder from '../../cardbuilder/cardBuilder';
|
||||||
|
|
||||||
|
const createLinkElement = ({ user, renderImgUrl }) => ({
|
||||||
|
__html: `<a
|
||||||
|
is="emby-linkbutton"
|
||||||
|
class="cardContent"
|
||||||
|
href="#!/useredit.html?userId=${user.Id}"
|
||||||
|
>
|
||||||
|
${renderImgUrl}
|
||||||
|
</a>`
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButtonElement = () => ({
|
||||||
|
__html: `<button
|
||||||
|
is="paper-icon-button-light"
|
||||||
|
type="button"
|
||||||
|
class="btnUserMenu flex-shrink-zero"
|
||||||
|
>
|
||||||
|
<span class="material-icons more_vert"></span>
|
||||||
|
</button>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
user?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLastSeenText = (lastActivityDate) => {
|
||||||
|
if (lastActivityDate) {
|
||||||
|
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
|
||||||
|
let cssClass = 'card squareCard scalableCard squareCard-scalable';
|
||||||
|
|
||||||
|
if (user.Policy.IsDisabled) {
|
||||||
|
cssClass += ' grayscale';
|
||||||
|
}
|
||||||
|
|
||||||
|
let imgUrl;
|
||||||
|
|
||||||
|
if (user.PrimaryImageTag) {
|
||||||
|
imgUrl = window.ApiClient.getUserImageUrl(user.Id, {
|
||||||
|
width: 300,
|
||||||
|
tag: user.PrimaryImageTag,
|
||||||
|
type: 'Primary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageClass = 'cardImage';
|
||||||
|
|
||||||
|
if (user.Policy.IsDisabled) {
|
||||||
|
imageClass += ' disabledUser';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSeen = getLastSeenText(user.LastActivityDate);
|
||||||
|
|
||||||
|
const renderImgUrl = imgUrl ?
|
||||||
|
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` :
|
||||||
|
`<div class='${imageClass} ${cardBuilder.getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'>
|
||||||
|
<span class='material-icons cardImageIcon person'></span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-userid={user.Id} className={cssClass}>
|
||||||
|
<div className='cardBox visualCardBox'>
|
||||||
|
<div className='cardScalable visualCardBox-cardScalable'>
|
||||||
|
<div className='cardPadder cardPadder-square'></div>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createLinkElement({
|
||||||
|
user: user,
|
||||||
|
renderImgUrl: renderImgUrl
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='cardFooter visualCardBox-cardFooter'>
|
||||||
|
<div className='cardText flex align-items-center'>
|
||||||
|
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||||
|
{user.Name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createButtonElement()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='cardText cardText-secondary'>
|
||||||
|
{lastSeen != '' ? lastSeen : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserCardBox;
|
|
@ -122,6 +122,8 @@
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,22 +10,20 @@ import '../formdialog.scss';
|
||||||
import '../../elements/emby-button/emby-button';
|
import '../../elements/emby-button/emby-button';
|
||||||
import alert from '../alert';
|
import alert from '../alert';
|
||||||
|
|
||||||
/* eslint-disable indent */
|
function getSystemInfo() {
|
||||||
|
|
||||||
function getSystemInfo() {
|
|
||||||
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
||||||
info => {
|
info => {
|
||||||
systemInfo = info;
|
systemInfo = info;
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDialogClosed() {
|
function onDialogClosed() {
|
||||||
loading.hide();
|
loading.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
||||||
if (path && typeof path !== 'string') {
|
if (path && typeof path !== 'string') {
|
||||||
throw new Error('invalid path');
|
throw new Error('invalid path');
|
||||||
}
|
}
|
||||||
|
@ -34,16 +32,12 @@ import alert from '../alert';
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
if (path === 'Network') {
|
|
||||||
promises.push(ApiClient.getNetworkDevices());
|
|
||||||
} else {
|
|
||||||
if (path) {
|
if (path) {
|
||||||
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
||||||
promises.push(ApiClient.getParentPath(path));
|
promises.push(ApiClient.getParentPath(path));
|
||||||
} else {
|
} else {
|
||||||
promises.push(ApiClient.getDrives());
|
promises.push(ApiClient.getDrives());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises).then(
|
Promise.all(promises).then(
|
||||||
responses => {
|
responses => {
|
||||||
|
@ -63,10 +57,6 @@ import alert from '../alert';
|
||||||
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
|
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
html += getItem('lnkPath lnkDirectory', '', 'Network', globalize.translate('ButtonNetwork'));
|
|
||||||
}
|
|
||||||
|
|
||||||
page.querySelector('.results').innerHTML = html;
|
page.querySelector('.results').innerHTML = html;
|
||||||
loading.hide();
|
loading.hide();
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -77,9 +67,9 @@ import alert from '../alert';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItem(cssClass, type, path, name) {
|
function getItem(cssClass, type, path, name) {
|
||||||
let html = '';
|
let html = '';
|
||||||
html += `<div class="listItem listItem-border ${cssClass}" data-type="${type}" data-path="${path}">`;
|
html += `<div class="listItem listItem-border ${cssClass}" data-type="${type}" data-path="${path}">`;
|
||||||
html += '<div class="listItemBody" style="padding-left:0;padding-top:.5em;padding-bottom:.5em;">';
|
html += '<div class="listItemBody" style="padding-left:0;padding-top:.5em;padding-bottom:.5em;">';
|
||||||
|
@ -90,9 +80,9 @@ import alert from '../alert';
|
||||||
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>';
|
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditorHtml(options, systemInfo) {
|
function getEditorHtml(options, systemInfo) {
|
||||||
let html = '';
|
let html = '';
|
||||||
html += '<div class="formDialogContent scrollY">';
|
html += '<div class="formDialogContent scrollY">';
|
||||||
html += '<div class="dialogContentInner dialog-content-centered" style="padding-top:2em;">';
|
html += '<div class="dialogContentInner dialog-content-centered" style="padding-top:2em;">';
|
||||||
|
@ -149,19 +139,19 @@ import alert from '../alert';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function alertText(text) {
|
function alertText(text) {
|
||||||
alertTextWithOptions({
|
alertTextWithOptions({
|
||||||
text: text
|
text: text
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function alertTextWithOptions(options) {
|
function alertTextWithOptions(options) {
|
||||||
alert(options);
|
alert(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePath(path, validateWriteable, apiClient) {
|
function validatePath(path, validateWriteable, apiClient) {
|
||||||
return apiClient.ajax({
|
return apiClient.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: apiClient.getUrl('Environment/ValidatePath'),
|
url: apiClient.getUrl('Environment/ValidatePath'),
|
||||||
|
@ -187,9 +177,9 @@ import alert from '../alert';
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initEditor(content, options, fileOptions) {
|
function initEditor(content, options, fileOptions) {
|
||||||
content.addEventListener('click', e => {
|
content.addEventListener('click', e => {
|
||||||
const lnkPath = dom.parentWithClass(e.target, 'lnkPath');
|
const lnkPath = dom.parentWithClass(e.target, 'lnkPath');
|
||||||
if (lnkPath) {
|
if (lnkPath) {
|
||||||
|
@ -227,9 +217,9 @@ import alert from '../alert';
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultPath(options) {
|
function getDefaultPath(options) {
|
||||||
if (options.path) {
|
if (options.path) {
|
||||||
return Promise.resolve(options.path);
|
return Promise.resolve(options.path);
|
||||||
} else {
|
} else {
|
||||||
|
@ -241,12 +231,13 @@ import alert from '../alert';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class directoryBrowser {
|
let systemInfo;
|
||||||
constructor() {
|
class DirectoryBrowser {
|
||||||
let currentDialog;
|
currentDialog;
|
||||||
this.show = options => {
|
|
||||||
|
show = options => {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const fileOptions = {
|
const fileOptions = {
|
||||||
includeDirectories: true
|
includeDirectories: true
|
||||||
|
@ -286,7 +277,7 @@ import alert from '../alert';
|
||||||
dlg.querySelector('.btnCloseDialog').addEventListener('click', () => {
|
dlg.querySelector('.btnCloseDialog').addEventListener('click', () => {
|
||||||
dialogHelper.close(dlg);
|
dialogHelper.close(dlg);
|
||||||
});
|
});
|
||||||
currentDialog = dlg;
|
this.currentDialog = dlg;
|
||||||
dlg.querySelector('#txtDirectoryPickerPath').value = initialPath;
|
dlg.querySelector('#txtDirectoryPickerPath').value = initialPath;
|
||||||
const txtNetworkPath = dlg.querySelector('#txtNetworkPath');
|
const txtNetworkPath = dlg.querySelector('#txtNetworkPath');
|
||||||
if (txtNetworkPath) {
|
if (txtNetworkPath) {
|
||||||
|
@ -298,15 +289,12 @@ import alert from '../alert';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
this.close = () => {
|
|
||||||
if (currentDialog) {
|
close = () => {
|
||||||
dialogHelper.close(currentDialog);
|
if (this.currentDialog) {
|
||||||
|
dialogHelper.close(this.currentDialog);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let systemInfo;
|
export default DirectoryBrowser;
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
||||||
export default directoryBrowser;
|
|
||||||
|
|
|
@ -132,6 +132,7 @@ import template from './displaySettings.template.html';
|
||||||
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
||||||
|
|
||||||
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
||||||
|
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
|
||||||
|
|
||||||
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
||||||
|
|
||||||
|
@ -156,6 +157,7 @@ import template from './displaySettings.template.html';
|
||||||
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
||||||
|
|
||||||
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
||||||
|
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
|
||||||
|
|
||||||
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
||||||
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
||||||
|
|
|
@ -7,65 +7,75 @@
|
||||||
<select id="selectLanguage" is="emby-select" label="${LabelDisplayLanguage}">
|
<select id="selectLanguage" is="emby-select" label="${LabelDisplayLanguage}">
|
||||||
<option value="">${Auto}</option>
|
<option value="">${Auto}</option>
|
||||||
<option value="af">Afrikaans</option>
|
<option value="af">Afrikaans</option>
|
||||||
<option value="sq">Albanian</option>
|
<option value="ar">العربية</option>
|
||||||
<option value="ar">Arabic</option>
|
<option value="be-BY">Беларуская</option>
|
||||||
<option value="be-BY">Belarusian</option>
|
<option value="bg-BG">Български</option>
|
||||||
<option value="bn_BD">Bengali (Bangladesh)</option>
|
<option value="bn_BD">বাংলা (বাংলাদেশ)</option>
|
||||||
<option value="bg-BG">Bulgarian</option>
|
<option value="ca">Català</option>
|
||||||
<option value="ca">Catalan</option>
|
<option value="cs">Čeština</option>
|
||||||
<option value="zh-HK">Chinese (Hong Kong)</option>
|
<option value="cy">Cymraeg</option>
|
||||||
<option value="zh-CN">Chinese (Simplified)</option>
|
<option value="da">Dansk</option>
|
||||||
<option value="zh-TW">Chinese (Traditional)</option>
|
<option value="de">Deutsch</option>
|
||||||
<option value="hr">Croatian</option>
|
<option value="el">Ελληνικά</option>
|
||||||
<option value="cs">Czech</option>
|
|
||||||
<option value="da">Danish</option>
|
|
||||||
<option value="nl">Dutch</option>
|
|
||||||
<option value="en-US">English</option>
|
|
||||||
<option value="en-GB">English (United Kingdom)</option>
|
<option value="en-GB">English (United Kingdom)</option>
|
||||||
|
<option value="en-US">English</option>
|
||||||
<option value="eo">Esperanto</option>
|
<option value="eo">Esperanto</option>
|
||||||
|
<option value="es">Español</option>
|
||||||
|
<option value="es_419">Español americano</option>
|
||||||
|
<option value="es-AR">Español (Argentina)</option>
|
||||||
|
<option value="es_DO">Español (Dominicana)</option>
|
||||||
|
<option value="es-MX">Español (México)</option>
|
||||||
|
<option value="et">Eesti</option>
|
||||||
|
<option value="fa">فارسی</option>
|
||||||
|
<option value="fi">Suomi</option>
|
||||||
<option value="fil">Filipino</option>
|
<option value="fil">Filipino</option>
|
||||||
<option value="fi">Finnish</option>
|
<option value="fr">Français</option>
|
||||||
<option value="fr">French</option>
|
<option value="fr-CA">Français (Canada)</option>
|
||||||
<option value="fr-CA">French (Canada)</option>
|
<option value="gl">Galego</option>
|
||||||
<option value="gl">Galician</option>
|
<option value="gsw">Schwiizerdütsch</option>
|
||||||
<option value="de">German</option>
|
<option value="he">עִבְרִית</option>
|
||||||
<option value="gsw">German (Swiss)</option>
|
<option value="hi-IN">हिन्दी</option>
|
||||||
<option value="el">Greek</option>
|
<option value="hr">Hrvatski </option>
|
||||||
<option value="he">Hebrew</option>
|
<option value="hu">Magyar</option>
|
||||||
<option value="hi-IN">Hindi</option>
|
<option value="id">Bahasa Indonesia</option>
|
||||||
<option value="hu">Hungarian</option>
|
<option value="is-IS">Íslenska</option>
|
||||||
<option value="is">Icelandic</option>
|
<option value="it">Italiano</option>
|
||||||
<option value="id">Indonesian</option>
|
<option value="ja">日本語</option>
|
||||||
<option value="it">Italian</option>
|
<option value="kk">Qazaqşa</option>
|
||||||
<option value="ja">Japanese</option>
|
<option value="ko">한국어</option>
|
||||||
<option value="kk">Kazakh</option>
|
<option value="lt-LT">Lietuvių</option>
|
||||||
<option value="ko">Korean</option>
|
<option value="lv">Latviešu</option>
|
||||||
<option value="lt-LT">Lithuanian</option>
|
<option value="mk">Македонски</option>
|
||||||
<option value="ms">Malay</option>
|
<option value="ml">മലയാളം</option>
|
||||||
<option value="mr">Marathi</option>
|
<option value="mr">मराठी</option>
|
||||||
<option value="nb">Norwegian Bokmål</option>
|
<option value="ms">Bahasa Melayu</option>
|
||||||
<option value="fa">Persian</option>
|
<option value="nb">Norsk bokmål</option>
|
||||||
|
<option value="ne">नेपाली</option>
|
||||||
|
<option value="nl">Nederlands</option>
|
||||||
|
<option value="nn">Norsk nynorsk</option>
|
||||||
|
<option value="pa">ਪੰਜਾਬੀ</option>
|
||||||
|
<option value="pl">Polski</option>
|
||||||
<option value="pr">Pirate</option>
|
<option value="pr">Pirate</option>
|
||||||
<option value="pl">Polish</option>
|
<option value="pt">Português</option>
|
||||||
<option value="pt">Portuguese</option>
|
<option value="pt-BR">Português (Brasil)</option>
|
||||||
<option value="pt-BR">Portuguese (Brazil)</option>
|
<option value="pt-PT">Português (Portugal)</option>
|
||||||
<option value="pt-PT">Portuguese (Portugal)</option>
|
<option value="ro">Românește</option>
|
||||||
<option value="ro">Romanian</option>
|
<option value="ru">Русский</option>
|
||||||
<option value="ru">Russian</option>
|
<option value="sk">Slovenčina</option>
|
||||||
<option value="sk">Slovak</option>
|
<option value="sl-SI">Slovenščina</option>
|
||||||
<option value="sl-SI">Slovenian (Slovenia)</option>
|
<option value="sq">Shqip</option>
|
||||||
<option value="es">Spanish</option>
|
<option value="sr">Српски</option>
|
||||||
<option value="es_AR">Spanish (Argentina)</option>
|
<option value="sv">Svenska</option>
|
||||||
<option value="es_DO">Spanish (Dominican Republic)</option>
|
<option value="ta">தமிழ்</option>
|
||||||
<option value="es-419">Spanish (Latin America)</option>
|
<option value="te">తెలుగు</option>
|
||||||
<option value="es-MX">Spanish (Mexico)</option>
|
<option value="th">ภาษาไทย</option>
|
||||||
<option value="sv">Swedish</option>
|
<option value="tr">Türkçe</option>
|
||||||
<option value="ta">Tamil</option>
|
<option value="uk">Українська</option>
|
||||||
<option value="th">Thai</option>
|
<option value="ur_PK">اُردُو</option>
|
||||||
<option value="tr">Turkish</option>
|
<option value="vi">Tiếng Việt</option>
|
||||||
<option value="uk">Ukrainian</option>
|
<option value="zh-CN">汉语 (简化字)</option>
|
||||||
<option value="ur_PK">Urdu (Pakistan)</option>
|
<option value="zh-TW">漢語 (繁体字)</option>
|
||||||
<option value="vi">Vietnamese</option>
|
<option value="zh-HK">廣東話 (香港)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
<div>${LabelDisplayLanguageHelp}</div>
|
<div>${LabelDisplayLanguageHelp}</div>
|
||||||
|
@ -79,65 +89,75 @@
|
||||||
<select is="emby-select" class="selectDateTimeLocale" label="${LabelDateTimeLocale}">
|
<select is="emby-select" class="selectDateTimeLocale" label="${LabelDateTimeLocale}">
|
||||||
<option value="">${Auto}</option>
|
<option value="">${Auto}</option>
|
||||||
<option value="af">Afrikaans</option>
|
<option value="af">Afrikaans</option>
|
||||||
<option value="sq">Albanian</option>
|
<option value="ar">العربية</option>
|
||||||
<option value="ar">Arabic</option>
|
<option value="be-BY">Беларуская</option>
|
||||||
<option value="be-BY">Belarusian</option>
|
<option value="bg-BG">Български</option>
|
||||||
<option value="bn_BD">Bengali (Bangladesh)</option>
|
<option value="bn_BD">বাংলা (বাংলাদেশ)</option>
|
||||||
<option value="bg-BG">Bulgarian</option>
|
<option value="ca">Català</option>
|
||||||
<option value="ca">Catalan</option>
|
<option value="cs">Čeština</option>
|
||||||
<option value="zh-HK">Chinese (Hong Kong)</option>
|
<option value="cy">Cymraeg</option>
|
||||||
<option value="zh-CN">Chinese (Simplified)</option>
|
<option value="da">Dansk</option>
|
||||||
<option value="zh-TW">Chinese (Traditional)</option>
|
<option value="de">Deutsch</option>
|
||||||
<option value="hr">Croatian</option>
|
<option value="el">Ελληνικά</option>
|
||||||
<option value="cs">Czech</option>
|
|
||||||
<option value="da">Danish</option>
|
|
||||||
<option value="nl">Dutch</option>
|
|
||||||
<option value="en-US">English</option>
|
|
||||||
<option value="en-GB">English (United Kingdom)</option>
|
<option value="en-GB">English (United Kingdom)</option>
|
||||||
|
<option value="en-US">English</option>
|
||||||
<option value="eo">Esperanto</option>
|
<option value="eo">Esperanto</option>
|
||||||
|
<option value="es">Español</option>
|
||||||
|
<option value="es_419">Español americano</option>
|
||||||
|
<option value="es-AR">Español (Argentina)</option>
|
||||||
|
<option value="es_DO">Español (Dominicana)</option>
|
||||||
|
<option value="es-MX">Español (México)</option>
|
||||||
|
<option value="et">Eesti</option>
|
||||||
|
<option value="fa">فارسی</option>
|
||||||
|
<option value="fi">Suomi</option>
|
||||||
<option value="fil">Filipino</option>
|
<option value="fil">Filipino</option>
|
||||||
<option value="fi">Finnish</option>
|
<option value="fr">Français</option>
|
||||||
<option value="fr">French</option>
|
<option value="fr-CA">Français (Canada)</option>
|
||||||
<option value="fr-CA">French (Canada)</option>
|
<option value="gl">Galego</option>
|
||||||
<option value="gl">Galician</option>
|
<option value="gsw">Schwiizerdütsch</option>
|
||||||
<option value="de">German</option>
|
<option value="he">עִבְרִית</option>
|
||||||
<option value="gsw">German (Swiss)</option>
|
<option value="hi-IN">हिन्दी</option>
|
||||||
<option value="el">Greek</option>
|
<option value="hr">Hrvatski </option>
|
||||||
<option value="he">Hebrew</option>
|
<option value="hu">Magyar</option>
|
||||||
<option value="hi-IN">Hindi</option>
|
<option value="id">Bahasa Indonesia</option>
|
||||||
<option value="hu">Hungarian</option>
|
<option value="is-IS">Íslenska</option>
|
||||||
<option value="is">Icelandic</option>
|
<option value="it">Italiano</option>
|
||||||
<option value="id">Indonesian</option>
|
<option value="ja">日本語</option>
|
||||||
<option value="it">Italian</option>
|
<option value="kk">Qazaqşa</option>
|
||||||
<option value="ja">Japanese</option>
|
<option value="ko">한국어</option>
|
||||||
<option value="kk">Kazakh</option>
|
<option value="lt-LT">Lietuvių</option>
|
||||||
<option value="ko">Korean</option>
|
<option value="lv">Latviešu</option>
|
||||||
<option value="lt-LT">Lithuanian</option>
|
<option value="mk">Македонски</option>
|
||||||
<option value="ms">Malay</option>
|
<option value="ml">മലയാളം</option>
|
||||||
<option value="mr">Marathi</option>
|
<option value="mr">मराठी</option>
|
||||||
<option value="nb">Norwegian Bokmål</option>
|
<option value="ms">Bahasa Melayu</option>
|
||||||
<option value="fa">Persian</option>
|
<option value="nb">Norsk bokmål</option>
|
||||||
|
<option value="ne">नेपाली</option>
|
||||||
|
<option value="nl">Nederlands</option>
|
||||||
|
<option value="nn">Norsk nynorsk</option>
|
||||||
|
<option value="pa">ਪੰਜਾਬੀ</option>
|
||||||
|
<option value="pl">Polski</option>
|
||||||
<option value="pr">Pirate</option>
|
<option value="pr">Pirate</option>
|
||||||
<option value="pl">Polish</option>
|
<option value="pt">Português</option>
|
||||||
<option value="pt">Portuguese</option>
|
<option value="pt-BR">Português (Brasil)</option>
|
||||||
<option value="pt-BR">Portuguese (Brazil)</option>
|
<option value="pt-PT">Português (Portugal)</option>
|
||||||
<option value="pt-PT">Portuguese (Portugal)</option>
|
<option value="ro">Românește</option>
|
||||||
<option value="ro">Romanian</option>
|
<option value="ru">Русский</option>
|
||||||
<option value="ru">Russian</option>
|
<option value="sk">Slovenčina</option>
|
||||||
<option value="sk">Slovak</option>
|
<option value="sl-SI">Slovenščina</option>
|
||||||
<option value="sl-SI">Slovenian (Slovenia)</option>
|
<option value="sq">Shqip</option>
|
||||||
<option value="es">Spanish</option>
|
<option value="sr">Српски</option>
|
||||||
<option value="es_AR">Spanish (Argentina)</option>
|
<option value="sv">Svenska</option>
|
||||||
<option value="es_DO">Spanish (Dominican Republic)</option>
|
<option value="ta">தமிழ்</option>
|
||||||
<option value="es-419">Spanish (Latin America)</option>
|
<option value="te">తెలుగు</option>
|
||||||
<option value="es-MX">Spanish (Mexico)</option>
|
<option value="th">ภาษาไทย</option>
|
||||||
<option value="sv">Swedish</option>
|
<option value="tr">Türkçe</option>
|
||||||
<option value="ta">Tamil</option>
|
<option value="uk">Українська</option>
|
||||||
<option value="th">Thai</option>
|
<option value="ur_PK">اُردُو</option>
|
||||||
<option value="tr">Turkish</option>
|
<option value="vi">Tiếng Việt</option>
|
||||||
<option value="uk">Ukrainian</option>
|
<option value="zh-CN">汉语 (简化字)</option>
|
||||||
<option value="ur_PK">Urdu (Pakistan)</option>
|
<option value="zh-TW">漢語 (繁体字)</option>
|
||||||
<option value="vi">Vietnamese</option>
|
<option value="zh-HK">廣東話 (香港)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -182,6 +202,11 @@
|
||||||
<div class="fieldDescription">${LabelLibraryPageSizeHelp}</div>
|
<div class="fieldDescription">${LabelLibraryPageSizeHelp}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer inputContainer-withDescription">
|
||||||
|
<input is="emby-input" type="number" id="txtMaxDaysForNextUp" pattern="[0-9]*" required="required" min="0" max="1000" step="1" label="${LabelMaxDaysForNextUp}" />
|
||||||
|
<div class="fieldDescription">${LabelMaxDaysForNextUpHelp}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkFadein" />
|
<input type="checkbox" is="emby-checkbox" id="chkFadein" />
|
||||||
|
|
|
@ -84,11 +84,6 @@
|
||||||
flex-basis: 12em;
|
flex-basis: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-tv .formDialogFooterItem {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formDialogFooterItem-vertical {
|
.formDialogFooterItem-vertical {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -532,6 +532,11 @@ import ServerConnections from '../ServerConnections';
|
||||||
section: 'guide'
|
section: 'guide'
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
}) + '" 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', {
|
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||||
serverId: apiClient.serverId()
|
serverId: apiClient.serverId()
|
||||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||||
|
@ -595,9 +600,11 @@ import ServerConnections from '../ServerConnections';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextUpFetchFn(serverId) {
|
function getNextUpFetchFn(serverId, userSettings) {
|
||||||
return function () {
|
return function () {
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
const apiClient = ServerConnections.getApiClient(serverId);
|
||||||
|
const oldestDateForNextUp = new Date();
|
||||||
|
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
|
||||||
return apiClient.getNextUpEpisodes({
|
return apiClient.getNextUpEpisodes({
|
||||||
Limit: enableScrollX() ? 24 : 15,
|
Limit: enableScrollX() ? 24 : 15,
|
||||||
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path',
|
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path',
|
||||||
|
@ -605,7 +612,8 @@ import ServerConnections from '../ServerConnections';
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
EnableTotalRecordCount: false,
|
EnableTotalRecordCount: false,
|
||||||
DisableFirstEpisode: true
|
DisableFirstEpisode: false,
|
||||||
|
NextUpDateCutoff: oldestDateForNextUp.toISOString()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -665,7 +673,7 @@ import ServerConnections from '../ServerConnections';
|
||||||
elem.innerHTML = html;
|
elem.innerHTML = html;
|
||||||
|
|
||||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||||
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId());
|
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings);
|
||||||
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
|
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
|
||||||
itemsContainer.parentContainer = elem;
|
itemsContainer.parentContainer = elem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,6 +185,12 @@ import { Events } from 'jellyfin-apiclient';
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetSrc(elem) {
|
||||||
|
elem.src = '';
|
||||||
|
elem.innerHTML = '';
|
||||||
|
elem.removeAttribute('src');
|
||||||
|
}
|
||||||
|
|
||||||
function onSuccessfulPlay(elem, onErrorFn) {
|
function onSuccessfulPlay(elem, onErrorFn) {
|
||||||
elem.addEventListener('error', onErrorFn);
|
elem.addEventListener('error', onErrorFn);
|
||||||
}
|
}
|
||||||
|
@ -344,9 +350,7 @@ import { Events } from 'jellyfin-apiclient';
|
||||||
export function onEndedInternal(instance, elem, onErrorFn) {
|
export function onEndedInternal(instance, elem, onErrorFn) {
|
||||||
elem.removeEventListener('error', onErrorFn);
|
elem.removeEventListener('error', onErrorFn);
|
||||||
|
|
||||||
elem.src = '';
|
resetSrc(elem);
|
||||||
elem.innerHTML = '';
|
|
||||||
elem.removeAttribute('src');
|
|
||||||
|
|
||||||
destroyHlsPlayer(instance);
|
destroyHlsPlayer(instance);
|
||||||
destroyFlvPlayer(instance);
|
destroyFlvPlayer(instance);
|
||||||
|
|
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 lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
import { decode, isBlurhashValid } from 'blurhash';
|
|
||||||
import './style.scss';
|
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 */
|
/* eslint-disable indent */
|
||||||
|
|
||||||
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
||||||
|
@ -12,21 +27,7 @@ import './style.scss';
|
||||||
fillImageElement(elem, source);
|
fillImageElement(elem, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemBlurhashing(target, blurhashstr) {
|
function drawBlurhash(target, pixels, width, height) {
|
||||||
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,
|
|
||||||
// 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');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
@ -37,18 +38,35 @@ import './style.scss';
|
||||||
ctx.putImageData(imgData, 0, 0);
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
// This class is just an utility class, so users can customize the canvas using their own CSS.
|
||||||
canvas.classList.add('blurhash-canvas');
|
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.parentNode.insertBefore(canvas, target);
|
||||||
target.classList.add('blurhashed');
|
target.classList.add('blurhashed');
|
||||||
target.removeAttribute('data-blurhash');
|
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 = 20;
|
||||||
|
const height = 20;
|
||||||
|
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
|
||||||
|
targetDic[hash].push(target);
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
hash,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
target.classList.add('non-blurhashable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillImage(entry) {
|
export function fillImage(entry) {
|
||||||
|
@ -65,12 +83,23 @@ import './style.scss';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.intersectionRatio > 0) {
|
if (entry.intersectionRatio > 0) {
|
||||||
if (source) fillImageElement(target, source);
|
if (source) {
|
||||||
} else if (!source) {
|
fillImageElement(target, source);
|
||||||
requestAnimationFrame(() => {
|
|
||||||
emptyImageElement(target);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else if (!source) {
|
||||||
|
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) {
|
function fillImageElement(elem, url) {
|
||||||
|
@ -82,6 +111,7 @@ import './style.scss';
|
||||||
preloaderImg.src = url;
|
preloaderImg.src = url;
|
||||||
|
|
||||||
elem.classList.add('lazy-hidden');
|
elem.classList.add('lazy-hidden');
|
||||||
|
elem.addEventListener('animationend', onAnimationEnd);
|
||||||
|
|
||||||
preloaderImg.addEventListener('load', () => {
|
preloaderImg.addEventListener('load', () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
@ -92,23 +122,23 @@ import './style.scss';
|
||||||
}
|
}
|
||||||
elem.removeAttribute('data-src');
|
elem.removeAttribute('data-src');
|
||||||
|
|
||||||
elem.classList.remove('lazy-hidden');
|
|
||||||
if (userSettings.enableFastFadein()) {
|
if (userSettings.enableFastFadein()) {
|
||||||
elem.classList.add('lazy-image-fadein-fast');
|
elem.classList.add('lazy-image-fadein-fast');
|
||||||
} else {
|
} else {
|
||||||
elem.classList.add('lazy-image-fadein');
|
elem.classList.add('lazy-image-fadein');
|
||||||
}
|
}
|
||||||
|
elem.classList.remove('lazy-hidden');
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyImageElement(elem) {
|
function emptyImageElement(elem) {
|
||||||
|
elem.removeEventListener('animationend', onAnimationEnd);
|
||||||
|
const canvas = elem.previousSibling;
|
||||||
|
if (canvas && canvas.tagName === 'CANVAS') {
|
||||||
|
canvas.classList.remove('lazy-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
if (elem.tagName !== 'IMG') {
|
if (elem.tagName !== 'IMG') {
|
||||||
|
@ -122,16 +152,6 @@ import './style.scss';
|
||||||
|
|
||||||
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||||
elem.classList.add('lazy-hidden');
|
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) {
|
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 {
|
@keyframes fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
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;
|
animation: fadein 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lazy-blurhash-fadein {
|
.lazy-hidden {
|
||||||
animation: fadein 0.4s;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blurhash-canvas {
|
.blurhash-canvas {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function getDisplayName(item, options = {}) {
|
||||||
}
|
}
|
||||||
if (item.Type === 'Episode' && item.ParentIndexNumber === 0) {
|
if (item.Type === 'Episode' && item.ParentIndexNumber === 0) {
|
||||||
name = globalize.translate('ValueSpecialEpisodeName', name);
|
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 displayIndexNumber = item.IndexNumber;
|
||||||
|
|
||||||
let number = displayIndexNumber;
|
let number = displayIndexNumber;
|
||||||
|
|
|
@ -72,7 +72,7 @@ import template from './itemMediaInfo.template.html';
|
||||||
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`;
|
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`;
|
||||||
const attributes = [];
|
const attributes = [];
|
||||||
if (stream.DisplayTitle) {
|
if (stream.DisplayTitle) {
|
||||||
attributes.push(createAttribute('Title', stream.DisplayTitle));
|
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
|
||||||
}
|
}
|
||||||
if (stream.Language && stream.Type !== 'Video') {
|
if (stream.Language && stream.Type !== 'Video') {
|
||||||
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
|
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
|
||||||
|
|
|
@ -13,7 +13,10 @@
|
||||||
callback(entry);
|
callback(entry);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{rootMargin: '25%'});
|
{
|
||||||
|
rootMargin: '50%',
|
||||||
|
threshold: 0
|
||||||
|
});
|
||||||
|
|
||||||
this.observer = observer;
|
this.observer = observer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ import template from './libraryoptionseditor.template.html';
|
||||||
if (!plugins.length) return html;
|
if (!plugins.length) return html;
|
||||||
|
|
||||||
html += '<div class="metadataFetcher" data-type="' + availableTypeOptions.Type + '">';
|
html += '<div class="metadataFetcher" data-type="' + availableTypeOptions.Type + '">';
|
||||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('LabelTypeMetadataDownloaders', globalize.translate(availableTypeOptions.Type)) + '</h3>';
|
html += '<h3 class="checkboxListLabel">' + globalize.translate('LabelTypeMetadataDownloaders', globalize.translate('TypeOptionPlural' + availableTypeOptions.Type)) + '</h3>';
|
||||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||||
|
|
||||||
plugins.forEach((plugin, index) => {
|
plugins.forEach((plugin, index) => {
|
||||||
|
@ -218,7 +218,7 @@ import template from './libraryoptionseditor.template.html';
|
||||||
|
|
||||||
html += '<div class="imageFetcher" data-type="' + availableTypeOptions.Type + '">';
|
html += '<div class="imageFetcher" data-type="' + availableTypeOptions.Type + '">';
|
||||||
html += '<div class="flex align-items-center" style="margin:1.5em 0 .5em;">';
|
html += '<div class="flex align-items-center" style="margin:1.5em 0 .5em;">';
|
||||||
html += '<h3 class="checkboxListLabel" style="margin:0;">' + globalize.translate('HeaderTypeImageFetchers', availableTypeOptions.Type) + '</h3>';
|
html += '<h3 class="checkboxListLabel" style="margin:0;">' + globalize.translate('HeaderTypeImageFetchers', globalize.translate('TypeOptionPlural' + availableTypeOptions.Type)) + '</h3>';
|
||||||
const supportedImageTypes = availableTypeOptions.SupportedImageTypes || [];
|
const supportedImageTypes = availableTypeOptions.SupportedImageTypes || [];
|
||||||
if (supportedImageTypes.length > 1 || supportedImageTypes.length === 1 && supportedImageTypes[0] !== 'Primary') {
|
if (supportedImageTypes.length > 1 || supportedImageTypes.length === 1 && supportedImageTypes[0] !== 'Primary') {
|
||||||
html += '<button is="emby-button" class="raised btnImageOptionsForType" type="button" style="margin-left:1.5em;font-size:90%;"><span>' + globalize.translate('HeaderFetcherSettings') + '</span></button>';
|
html += '<button is="emby-button" class="raised btnImageOptionsForType" type="button" style="margin-left:1.5em;font-size:90%;"><span>' + globalize.translate('HeaderFetcherSettings') + '</span></button>';
|
||||||
|
@ -411,6 +411,8 @@ import template from './libraryoptionseditor.template.html';
|
||||||
parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide');
|
parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies');
|
||||||
|
|
||||||
return populateMetadataSettings(parent, contentType);
|
return populateMetadataSettings(parent, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,6 +513,7 @@ import template from './libraryoptionseditor.template.html';
|
||||||
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
|
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
|
||||||
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
|
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
|
||||||
RequirePerfectSubtitleMatch: parent.querySelector('#chkRequirePerfectMatch').checked,
|
RequirePerfectSubtitleMatch: parent.querySelector('#chkRequirePerfectMatch').checked,
|
||||||
|
AutomaticallyAddToCollection: parent.querySelector('#chkAutomaticallyAddToCollection').checked,
|
||||||
MetadataSavers: Array.prototype.map.call(Array.prototype.filter.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
MetadataSavers: Array.prototype.map.call(Array.prototype.filter.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
||||||
return elem.checked;
|
return elem.checked;
|
||||||
}), elem => {
|
}), elem => {
|
||||||
|
@ -562,6 +565,7 @@ import template from './libraryoptionseditor.template.html';
|
||||||
parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
|
parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
|
||||||
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;
|
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;
|
||||||
parent.querySelector('#chkRequirePerfectMatch').checked = options.RequirePerfectSubtitleMatch;
|
parent.querySelector('#chkRequirePerfectMatch').checked = options.RequirePerfectSubtitleMatch;
|
||||||
|
parent.querySelector('#chkAutomaticallyAddToCollection').checked = options.AutomaticallyAddToCollection;
|
||||||
Array.prototype.forEach.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
Array.prototype.forEach.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
||||||
elem.checked = options.MetadataSavers ? options.MetadataSavers.includes(elem.getAttribute('data-pluginname')) : elem.getAttribute('data-defaultenabled') === 'true';
|
elem.checked = options.MetadataSavers ? options.MetadataSavers.includes(elem.getAttribute('data-pluginname')) : elem.getAttribute('data-defaultenabled') === 'true';
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,14 @@
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableRealtimeMonitorHelp}</div>
|
<div class="fieldDescription checkboxFieldDescription">${LabelEnableRealtimeMonitorHelp}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription chkAutomaticallyAddToCollectionContainer hide advanced">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="chkAutomaticallyAddToCollection" checked />
|
||||||
|
<span>${LabelAutomaticallyAddToCollection}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${LabelAutomaticallyAddToCollectionHelp}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="metadataReaders hide advanced" style="margin-bottom: 2em;">
|
<div class="metadataReaders hide advanced" style="margin-bottom: 2em;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -133,21 +133,28 @@ import ServerConnections from '../ServerConnections';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let elem;
|
||||||
|
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
if (isLargeStyle) {
|
if (isLargeStyle) {
|
||||||
html += `<${largeTitleTagName} class="listItemBodyText">`;
|
elem = document.createElement(largeTitleTagName);
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="listItemBodyText">';
|
elem = document.createElement('div');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="secondary listItemBodyText">';
|
elem = document.createElement('div');
|
||||||
|
elem.classList.add('secondary');
|
||||||
}
|
}
|
||||||
html += (textlines[i] || ' ');
|
|
||||||
if (i === 0 && isLargeStyle) {
|
elem.classList.add('listItemBodyText');
|
||||||
html += `</${largeTitleTagName}>`;
|
|
||||||
|
if (textlines[i]) {
|
||||||
|
elem.innerText = textlines[i];
|
||||||
} else {
|
} else {
|
||||||
html += '</div>';
|
elem.innerHTML = ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html += elem.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-1 {
|
.mdl-spinner__layer-1 {
|
||||||
border-color: rgb(66, 165, 245);
|
border-color: #00a4dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-1-active {
|
.mdl-spinner__layer-1-active {
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-2 {
|
.mdl-spinner__layer-2 {
|
||||||
border-color: rgb(244, 67, 54);
|
border-color: #00a4dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-2-active {
|
.mdl-spinner__layer-2-active {
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-3 {
|
.mdl-spinner__layer-3 {
|
||||||
border-color: rgb(253, 216, 53);
|
border-color: #00a4dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-3-active {
|
.mdl-spinner__layer-3-active {
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-4 {
|
.mdl-spinner__layer-4 {
|
||||||
border-color: rgb(76, 175, 80);
|
border-color: #00a4dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdl-spinner__layer-4-active {
|
.mdl-spinner__layer-4-active {
|
||||||
|
|
|
@ -102,8 +102,8 @@ import template from './mediaLibraryCreator.template.html';
|
||||||
function onAddButtonClick() {
|
function onAddButtonClick() {
|
||||||
const page = dom.parentWithClass(this, 'dlg-librarycreator');
|
const page = dom.parentWithClass(this, 'dlg-librarycreator');
|
||||||
|
|
||||||
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
enableNetworkSharePath: true,
|
enableNetworkSharePath: true,
|
||||||
callback: function (path, networkSharePath) {
|
callback: function (path, networkSharePath) {
|
||||||
|
|
|
@ -162,8 +162,8 @@ import template from './mediaLibraryEditor.template.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDirectoryBrowser(context, originalPath, networkPath) {
|
function showDirectoryBrowser(context, originalPath, networkPath) {
|
||||||
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
enableNetworkSharePath: true,
|
enableNetworkSharePath: true,
|
||||||
pathReadOnly: originalPath != null,
|
pathReadOnly: originalPath != null,
|
||||||
|
|
|
@ -106,10 +106,9 @@ import '../../elements/emby-button/emby-button';
|
||||||
const miscInfo = [];
|
const miscInfo = [];
|
||||||
let text;
|
let text;
|
||||||
let date;
|
let date;
|
||||||
let minutes;
|
|
||||||
let count;
|
let count;
|
||||||
|
|
||||||
const showFolderRuntime = item.Type === 'MusicAlbum' || item.MediaType === 'MusicArtist' || item.MediaType === 'Playlist' || item.MediaType === 'MusicGenre';
|
const showFolderRuntime = item.Type === 'MusicAlbum' || item.MediaType === 'MusicArtist' || item.Type === 'Playlist' || item.MediaType === 'Playlist' || item.MediaType === 'MusicGenre';
|
||||||
|
|
||||||
if (showFolderRuntime) {
|
if (showFolderRuntime) {
|
||||||
count = item.SongCount || item.ChildCount;
|
count = item.SongCount || item.ChildCount;
|
||||||
|
@ -119,7 +118,7 @@ import '../../elements/emby-button/emby-button';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.RunTimeTicks) {
|
if (item.RunTimeTicks) {
|
||||||
miscInfo.push(datetime.getDisplayRunningTime(item.RunTimeTicks));
|
miscInfo.push(datetime.getDisplayDuration(item.RunTimeTicks));
|
||||||
}
|
}
|
||||||
} else if (item.Type === 'PhotoAlbum' || item.Type === 'BoxSet') {
|
} else if (item.Type === 'PhotoAlbum' || item.Type === 'BoxSet') {
|
||||||
count = item.ChildCount;
|
count = item.ChildCount;
|
||||||
|
@ -132,7 +131,8 @@ import '../../elements/emby-button/emby-button';
|
||||||
if ((item.Type === 'Episode' || item.MediaType === 'Photo') && options.originalAirDate !== false) {
|
if ((item.Type === 'Episode' || item.MediaType === 'Photo') && options.originalAirDate !== false) {
|
||||||
if (item.PremiereDate) {
|
if (item.PremiereDate) {
|
||||||
try {
|
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);
|
text = datetime.toLocaleDateString(date);
|
||||||
miscInfo.push(text);
|
miscInfo.push(text);
|
||||||
|
@ -257,11 +257,7 @@ import '../../elements/emby-button/emby-button';
|
||||||
if (item.Type === 'Audio') {
|
if (item.Type === 'Audio') {
|
||||||
miscInfo.push(datetime.getDisplayRunningTime(item.RunTimeTicks));
|
miscInfo.push(datetime.getDisplayRunningTime(item.RunTimeTicks));
|
||||||
} else {
|
} else {
|
||||||
minutes = item.RunTimeTicks / 600000000;
|
miscInfo.push(datetime.getDisplayDuration(item.RunTimeTicks));
|
||||||
|
|
||||||
minutes = minutes || 1;
|
|
||||||
|
|
||||||
miscInfo.push(`${Math.round(minutes)} mins`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -520,7 +520,7 @@ import template from './metadataEditor.template.html';
|
||||||
hideElement('#fldPath', context);
|
hideElement('#fldPath', context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Type === 'Series' || item.Type === 'Movie' || item.Type === 'Trailer') {
|
if (item.Type === 'Series' || item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Person') {
|
||||||
showElement('#fldOriginalName', context);
|
showElement('#fldOriginalName', context);
|
||||||
} else {
|
} else {
|
||||||
hideElement('#fldOriginalName', context);
|
hideElement('#fldOriginalName', context);
|
||||||
|
@ -637,7 +637,9 @@ import template from './metadataEditor.template.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Type === 'Person') {
|
if (item.Type === 'Person') {
|
||||||
//todo
|
context.querySelector('#txtName').label(globalize.translate('LabelName'));
|
||||||
|
context.querySelector('#txtSortName').label(globalize.translate('LabelSortName'));
|
||||||
|
context.querySelector('#txtOriginalName').label(globalize.translate('LabelOriginalName'));
|
||||||
context.querySelector('#txtProductionYear').label(globalize.translate('LabelBirthYear'));
|
context.querySelector('#txtProductionYear').label(globalize.translate('LabelBirthYear'));
|
||||||
context.querySelector('#txtPremiereDate').label(globalize.translate('LabelBirthDate'));
|
context.querySelector('#txtPremiereDate').label(globalize.translate('LabelBirthDate'));
|
||||||
context.querySelector('#txtEndDate').label(globalize.translate('LabelDeathDate'));
|
context.querySelector('#txtEndDate').label(globalize.translate('LabelDeathDate'));
|
||||||
|
|
|
@ -252,7 +252,7 @@
|
||||||
<br />
|
<br />
|
||||||
<div class="formDialogFooter">
|
<div class="formDialogFooter">
|
||||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel formDialogFooterItem">
|
<button is="emby-button" type="button" class="raised button-cancel block btnCancel formDialogFooterItem">
|
||||||
<span>${Cancel}</span>
|
<span>${ButtonCancel}</span>
|
||||||
</button>
|
</button>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block btnSave formDialogFooterItem">
|
<button is="emby-button" type="submit" class="raised button-submit block btnSave formDialogFooterItem">
|
||||||
<span>${SaveChanges}</span>
|
<span>${SaveChanges}</span>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ServerConnections from '../ServerConnections';
|
||||||
import alert from '../alert';
|
import alert from '../alert';
|
||||||
import playlistEditor from '../playlisteditor/playlisteditor';
|
import playlistEditor from '../playlisteditor/playlisteditor';
|
||||||
import confirm from '../confirm/confirm';
|
import confirm from '../confirm/confirm';
|
||||||
|
import itemHelper from '../itemHelper';
|
||||||
|
|
||||||
/* eslint-disable indent */
|
/* eslint-disable indent */
|
||||||
|
|
||||||
|
@ -170,8 +171,16 @@ import confirm from '../confirm/confirm';
|
||||||
const apiClient = ServerConnections.currentApiClient();
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
apiClient.getCurrentUser().then(user => {
|
apiClient.getCurrentUser().then(user => {
|
||||||
|
// get first selected item to perform metadata refresh permission check
|
||||||
|
apiClient.getItem(apiClient.getCurrentUserId(), selectedItems[0]).then(firstItem => {
|
||||||
const menuItems = [];
|
const menuItems = [];
|
||||||
|
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('SelectAll'),
|
||||||
|
id: 'selectall',
|
||||||
|
icon: 'select_all'
|
||||||
|
});
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
name: globalize.translate('AddToCollection'),
|
name: globalize.translate('AddToCollection'),
|
||||||
id: 'addtocollection',
|
id: 'addtocollection',
|
||||||
|
@ -224,11 +233,15 @@ import confirm from '../confirm/confirm';
|
||||||
icon: 'check_box_outline_blank'
|
icon: 'check_box_outline_blank'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// this assues that if the user can refresh metadata for the first item
|
||||||
|
// they can refresh metadata for all items
|
||||||
|
if (itemHelper.canRefreshMetadata(firstItem, user)) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
name: globalize.translate('RefreshMetadata'),
|
name: globalize.translate('RefreshMetadata'),
|
||||||
id: 'refresh',
|
id: 'refresh',
|
||||||
icon: 'refresh'
|
icon: 'refresh'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
import('../actionSheet/actionSheet').then((actionsheet) => {
|
import('../actionSheet/actionSheet').then((actionsheet) => {
|
||||||
actionsheet.show({
|
actionsheet.show({
|
||||||
|
@ -239,6 +252,19 @@ import confirm from '../confirm/confirm';
|
||||||
const serverId = apiClient.serverInfo().Id;
|
const serverId = apiClient.serverInfo().Id;
|
||||||
|
|
||||||
switch (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':
|
case 'addtocollection':
|
||||||
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
||||||
new collectionEditor({
|
new collectionEditor({
|
||||||
|
@ -296,6 +322,7 @@ import confirm from '../confirm/confirm';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchNeedsRefresh() {
|
function dispatchNeedsRefresh() {
|
||||||
|
|
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 { Events } from 'jellyfin-apiclient';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
|
||||||
|
import NotificationIcon from './notificationicon.png';
|
||||||
|
|
||||||
function onOneDocumentClick() {
|
function onOneDocumentClick() {
|
||||||
document.removeEventListener('click', onOneDocumentClick);
|
document.removeEventListener('click', onOneDocumentClick);
|
||||||
document.removeEventListener('keydown', onOneDocumentClick);
|
document.removeEventListener('keydown', onOneDocumentClick);
|
||||||
|
@ -71,8 +73,8 @@ function showNotification(options, timeoutMs, apiClient) {
|
||||||
|
|
||||||
options.data = options.data || {};
|
options.data = options.data || {};
|
||||||
options.data.serverId = apiClient.serverInfo().Id;
|
options.data.serverId = apiClient.serverInfo().Id;
|
||||||
options.icon = options.icon || getIconUrl();
|
options.icon = options.icon || NotificationIcon;
|
||||||
options.badge = options.badge || getIconUrl('badge.png');
|
options.badge = options.badge || NotificationIcon;
|
||||||
|
|
||||||
resetRegistration();
|
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) {
|
function showPackageInstallNotification(apiClient, installation, status) {
|
||||||
apiClient.getCurrentUser().then(function (user) {
|
apiClient.getCurrentUser().then(function (user) {
|
||||||
if (!user.Policy.IsAdministrator) {
|
if (!user.Policy.IsAdministrator) {
|
||||||
|
@ -180,7 +177,7 @@ function showPackageInstallNotification(apiClient, installation, status) {
|
||||||
{
|
{
|
||||||
action: 'cancel-install',
|
action: 'cancel-install',
|
||||||
title: globalize.translate('ButtonCancel'),
|
title: globalize.translate('ButtonCancel'),
|
||||||
icon: getIconUrl()
|
icon: NotificationIcon
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -249,7 +246,7 @@ Events.on(serverNotifications, 'RestartRequired', function (e, apiClient) {
|
||||||
{
|
{
|
||||||
action: 'restart',
|
action: 'restart',
|
||||||
title: globalize.translate('Restart'),
|
title: globalize.translate('Restart'),
|
||||||
icon: getIconUrl()
|
icon: NotificationIcon
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
250
src/components/pages/NewUserPage.tsx
Normal file
250
src/components/pages/NewUserPage.tsx
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import Dashboard from '../../scripts/clientUtils';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import loading from '../loading/loading';
|
||||||
|
import toast from '../toast/toast';
|
||||||
|
|
||||||
|
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||||
|
import InputElement from '../dashboard/users/InputElement';
|
||||||
|
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||||
|
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||||
|
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||||
|
|
||||||
|
type userInput = {
|
||||||
|
Name?: string;
|
||||||
|
Password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemsArr = {
|
||||||
|
Name?: string;
|
||||||
|
Id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewUserPage: FunctionComponent = () => {
|
||||||
|
const [ channelsItems, setChannelsItems ] = useState([]);
|
||||||
|
const [ mediaFoldersItems, setMediaFoldersItems ] = useState([]);
|
||||||
|
const element = useRef(null);
|
||||||
|
|
||||||
|
const getItemsResult = (items: ItemsArr[]) => {
|
||||||
|
return items.map(item =>
|
||||||
|
({
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.Name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMediaFolders = useCallback((result) => {
|
||||||
|
const mediaFolders = getItemsResult(result);
|
||||||
|
|
||||||
|
setMediaFoldersItems(mediaFolders);
|
||||||
|
|
||||||
|
const folderAccess = element?.current?.querySelector('.folderAccess');
|
||||||
|
folderAccess.dispatchEvent(new CustomEvent('create'));
|
||||||
|
|
||||||
|
element.current.querySelector('.chkEnableAllFolders').checked = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadChannels = useCallback((result) => {
|
||||||
|
const channels = getItemsResult(result);
|
||||||
|
|
||||||
|
setChannelsItems(channels);
|
||||||
|
|
||||||
|
const channelAccess = element?.current?.querySelector('.channelAccess');
|
||||||
|
channelAccess.dispatchEvent(new CustomEvent('create'));
|
||||||
|
|
||||||
|
const channelAccessContainer = element?.current?.querySelector('.channelAccessContainer');
|
||||||
|
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
|
||||||
|
|
||||||
|
element.current.querySelector('.chkEnableAllChannels').checked = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadUser = useCallback(() => {
|
||||||
|
element.current.querySelector('#txtUsername').value = '';
|
||||||
|
element.current.querySelector('#txtPassword').value = '';
|
||||||
|
loading.show();
|
||||||
|
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||||
|
IsHidden: false
|
||||||
|
}));
|
||||||
|
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||||
|
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
||||||
|
loadMediaFolders(responses[0].Items);
|
||||||
|
loadChannels(responses[1].Items);
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
}, [loadChannels, loadMediaFolders]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUser();
|
||||||
|
|
||||||
|
const saveUser = () => {
|
||||||
|
const userInput: userInput = {};
|
||||||
|
userInput.Name = element?.current?.querySelector('#txtUsername').value;
|
||||||
|
userInput.Password = element?.current?.querySelector('#txtPassword').value;
|
||||||
|
window.ApiClient.createUser(userInput).then(function (user) {
|
||||||
|
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
|
||||||
|
user.Policy.EnabledFolders = [];
|
||||||
|
|
||||||
|
if (!user.Policy.EnableAllFolders) {
|
||||||
|
user.Policy.EnabledFolders = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (i) {
|
||||||
|
return i.checked;
|
||||||
|
}).map(function (i) {
|
||||||
|
return i.getAttribute('data-id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
|
||||||
|
user.Policy.EnabledChannels = [];
|
||||||
|
|
||||||
|
if (!user.Policy.EnableAllChannels) {
|
||||||
|
user.Policy.EnabledChannels = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (i) {
|
||||||
|
return i.checked;
|
||||||
|
}).map(function (i) {
|
||||||
|
return i.getAttribute('data-id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||||
|
Dashboard.navigate('useredit.html?userId=' + user.Id);
|
||||||
|
});
|
||||||
|
}, function () {
|
||||||
|
toast(globalize.translate('ErrorDefault'));
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (e) => {
|
||||||
|
loading.show();
|
||||||
|
saveUser();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
|
||||||
|
const channelAccessListContainer = element?.current?.querySelector('.channelAccessListContainer');
|
||||||
|
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
|
||||||
|
const folderAccessListContainer = element?.current?.querySelector('.folderAccessListContainer');
|
||||||
|
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
element?.current?.querySelector('.newUserProfileForm').addEventListener('submit', onSubmit);
|
||||||
|
|
||||||
|
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
|
||||||
|
window.history.back();
|
||||||
|
});
|
||||||
|
}, [loadUser]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='content-primary'>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='sectionTitleContainer flex align-items-center'>
|
||||||
|
<h2 className='sectionTitle'>
|
||||||
|
{globalize.translate('ButtonAddUser')}
|
||||||
|
</h2>
|
||||||
|
<SectionTitleLinkElement
|
||||||
|
className='raised button-alt headerHelpButton'
|
||||||
|
title='Help'
|
||||||
|
url='https://docs.jellyfin.org/general/server/users/'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form className='newUserProfileForm'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='text'
|
||||||
|
id='txtUsername'
|
||||||
|
label='LabelName'
|
||||||
|
options={'required'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='password'
|
||||||
|
id='txtPassword'
|
||||||
|
label='LabelPassword'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='folderAccessContainer'>
|
||||||
|
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
|
||||||
|
<CheckBoxElement
|
||||||
|
type='checkbox'
|
||||||
|
className='chkEnableAllFolders'
|
||||||
|
title='OptionEnableAccessToAllLibraries'
|
||||||
|
/>
|
||||||
|
<div className='folderAccessListContainer'>
|
||||||
|
<div className='folderAccess'>
|
||||||
|
<h3 className='checkboxListLabel'>
|
||||||
|
{globalize.translate('HeaderLibraries')}
|
||||||
|
</h3>
|
||||||
|
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||||
|
{mediaFoldersItems.map(Item => (
|
||||||
|
<CheckBoxListItem
|
||||||
|
key={Item.Id}
|
||||||
|
className='chkFolder'
|
||||||
|
Id={Item.Id}
|
||||||
|
Name={Item.Name}
|
||||||
|
checkedAttribute=''
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LibraryAccessHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='channelAccessContainer verticalSection-extrabottompadding hide'>
|
||||||
|
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
|
||||||
|
<CheckBoxElement
|
||||||
|
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=''
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('ChannelAccessHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 NewUserPage;
|
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;
|
154
src/components/pages/UserProfilesPage.tsx
Normal file
154
src/components/pages/UserProfilesPage.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
|
||||||
|
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
|
||||||
|
import Dashboard from '../../scripts/clientUtils';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import loading from '../loading/loading';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import confirm from '../../components/confirm/confirm';
|
||||||
|
import UserCardBox from '../dashboard/users/UserCardBox';
|
||||||
|
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
|
||||||
|
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../elements/emby-button/paper-icon-button-light';
|
||||||
|
import '../../components/cardbuilder/card.scss';
|
||||||
|
import '../../components/indicators/indicators.scss';
|
||||||
|
import '../../assets/css/flexstyles.scss';
|
||||||
|
|
||||||
|
type MenuEntry = {
|
||||||
|
name?: string;
|
||||||
|
id?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfilesPage: FunctionComponent = () => {
|
||||||
|
const [ users, setUsers ] = useState([]);
|
||||||
|
|
||||||
|
const element = useRef(null);
|
||||||
|
|
||||||
|
const loadData = () => {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getUsers().then(function (result) {
|
||||||
|
setUsers(result);
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
const showUserMenu = (elem) => {
|
||||||
|
const card = dom.parentWithClass(elem, 'card');
|
||||||
|
const userId = card.getAttribute('data-userid');
|
||||||
|
|
||||||
|
const menuItems: MenuEntry[] = [];
|
||||||
|
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('ButtonOpen'),
|
||||||
|
id: 'open',
|
||||||
|
icon: 'mode_edit'
|
||||||
|
});
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('ButtonLibraryAccess'),
|
||||||
|
id: 'access',
|
||||||
|
icon: 'lock'
|
||||||
|
});
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('ButtonParentalControl'),
|
||||||
|
id: 'parentalcontrol',
|
||||||
|
icon: 'person'
|
||||||
|
});
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('Delete'),
|
||||||
|
id: 'delete',
|
||||||
|
icon: 'delete'
|
||||||
|
});
|
||||||
|
|
||||||
|
import('../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||||
|
actionsheet.show({
|
||||||
|
items: menuItems,
|
||||||
|
positionTo: card,
|
||||||
|
callback: function (id) {
|
||||||
|
switch (id) {
|
||||||
|
case 'open':
|
||||||
|
Dashboard.navigate('useredit.html?userId=' + userId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'access':
|
||||||
|
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'parentalcontrol':
|
||||||
|
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
deleteUser(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = (id) => {
|
||||||
|
const msg = globalize.translate('DeleteUserConfirmation');
|
||||||
|
|
||||||
|
confirm({
|
||||||
|
title: globalize.translate('DeleteUser'),
|
||||||
|
text: msg,
|
||||||
|
confirmText: globalize.translate('Delete'),
|
||||||
|
primary: 'delete'
|
||||||
|
}).then(function () {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.deleteUser(id).then(function () {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
element?.current?.addEventListener('click', function (e) {
|
||||||
|
const btnUserMenu = dom.parentWithClass(e.target, 'btnUserMenu');
|
||||||
|
|
||||||
|
if (btnUserMenu) {
|
||||||
|
showUserMenu(btnUserMenu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
element?.current?.querySelector('.btnAddUser').addEventListener('click', function() {
|
||||||
|
Dashboard.navigate('usernew.html');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='content-primary'>
|
||||||
|
<div className='verticalSection verticalSection-extrabottompadding'>
|
||||||
|
<div
|
||||||
|
className='sectionTitleContainer sectionTitleContainer-cards'
|
||||||
|
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||||
|
>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards'>
|
||||||
|
{globalize.translate('HeaderUsers')}
|
||||||
|
</h2>
|
||||||
|
<SectionTitleButtonElement
|
||||||
|
className='fab btnAddUser submit sectionTitleButton'
|
||||||
|
title='ButtonAddUser'
|
||||||
|
icon='add'
|
||||||
|
/>
|
||||||
|
<SectionTitleLinkElement
|
||||||
|
className='raised button-alt headerHelpButton'
|
||||||
|
title='Help'
|
||||||
|
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='localUsers itemsContainer vertical-wrap'>
|
||||||
|
{users.map(user => {
|
||||||
|
return <UserCardBox key={user.Id} user={user} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfilesPage;
|
|
@ -8,7 +8,7 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import loading from '../loading/loading';
|
import loading from '../loading/loading';
|
||||||
import { appHost } from '../apphost';
|
import { appHost } from '../apphost';
|
||||||
import * as Screenfull from 'screenfull';
|
import Screenfull from 'screenfull';
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
import alert from '../alert';
|
import alert from '../alert';
|
||||||
|
|
||||||
|
@ -618,21 +618,6 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
|
||||||
} else {
|
} else {
|
||||||
return isHostReachable(mediaSource, apiClient);
|
return isHostReachable(mediaSource, apiClient);
|
||||||
}
|
}
|
||||||
} else if (mediaSource.Protocol === 'File') {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
// Determine if the file can be accessed directly
|
|
||||||
import('../../scripts/filesystem').then((filesystem) => {
|
|
||||||
const method = isFolderRip ?
|
|
||||||
'directoryExists' :
|
|
||||||
'fileExists';
|
|
||||||
|
|
||||||
filesystem[method](mediaSource.Path).then(function () {
|
|
||||||
resolve(true);
|
|
||||||
}, function () {
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1812,7 +1797,8 @@ class PlaybackManager {
|
||||||
// Setting this to true may cause some incorrect sorting
|
// Setting this to true may cause some incorrect sorting
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||||
MediaTypes: 'Photo,Video',
|
// Only include Photos because we do not handle mixed queues currently
|
||||||
|
MediaTypes: 'Photo',
|
||||||
Limit: 1000
|
Limit: 1000
|
||||||
});
|
});
|
||||||
} else if (firstItem.Type === 'MusicGenre') {
|
} else if (firstItem.Type === 'MusicGenre') {
|
||||||
|
@ -1823,6 +1809,16 @@ class PlaybackManager {
|
||||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||||
MediaTypes: 'Audio'
|
MediaTypes: 'Audio'
|
||||||
});
|
});
|
||||||
|
} else if (firstItem.IsFolder && firstItem.CollectionType === 'homevideos') {
|
||||||
|
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||||
|
ParentId: firstItem.Id,
|
||||||
|
Filters: 'IsNotFolder',
|
||||||
|
Recursive: true,
|
||||||
|
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||||
|
// Only include Photos because we do not handle mixed queues currently
|
||||||
|
MediaTypes: 'Photo',
|
||||||
|
Limit: 1000
|
||||||
|
}, queryOptions));
|
||||||
} else if (firstItem.IsFolder) {
|
} else if (firstItem.IsFolder) {
|
||||||
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
|
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||||
ParentId: firstItem.Id,
|
ParentId: firstItem.Id,
|
||||||
|
@ -2481,7 +2477,7 @@ class PlaybackManager {
|
||||||
// Only used for audio
|
// Only used for audio
|
||||||
playMethod = 'Transcode';
|
playMethod = 'Transcode';
|
||||||
mediaUrl = mediaSource.StreamUrl;
|
mediaUrl = mediaSource.StreamUrl;
|
||||||
} else if (mediaSource.SupportsDirectStream) {
|
} else if (mediaSource.SupportsDirectPlay || mediaSource.SupportsDirectStream) {
|
||||||
directOptions = {
|
directOptions = {
|
||||||
Static: true,
|
Static: true,
|
||||||
mediaSourceId: mediaSource.Id,
|
mediaSourceId: mediaSource.Id,
|
||||||
|
@ -2500,7 +2496,7 @@ class PlaybackManager {
|
||||||
const prefix = type === 'Video' ? 'Videos' : 'Audio';
|
const prefix = type === 'Video' ? 'Videos' : 'Audio';
|
||||||
mediaUrl = apiClient.getUrl(prefix + '/' + item.Id + '/stream.' + mediaSourceContainer, directOptions);
|
mediaUrl = apiClient.getUrl(prefix + '/' + item.Id + '/stream.' + mediaSourceContainer, directOptions);
|
||||||
|
|
||||||
playMethod = 'DirectStream';
|
playMethod = mediaSource.SupportsDirectPlay ? 'DirectPlay' : 'DirectStream';
|
||||||
} else if (mediaSource.SupportsTranscoding) {
|
} else if (mediaSource.SupportsTranscoding) {
|
||||||
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
|
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
|
||||||
|
|
||||||
|
@ -3068,7 +3064,9 @@ class PlaybackManager {
|
||||||
const data = getPlayerData(player);
|
const data = getPlayerData(player);
|
||||||
const streamInfo = data.streamInfo;
|
const streamInfo = data.streamInfo;
|
||||||
|
|
||||||
const nextItem = self._playNextAfterEnded ? self._playQueueManager.getNextItemInfo() : null;
|
const errorOccurred = displayErrorCode && typeof (displayErrorCode) === 'string';
|
||||||
|
|
||||||
|
const nextItem = self._playNextAfterEnded && !errorOccurred ? self._playQueueManager.getNextItemInfo() : null;
|
||||||
|
|
||||||
const nextMediaType = (nextItem ? nextItem.item.MediaType : null);
|
const nextMediaType = (nextItem ? nextItem.item.MediaType : null);
|
||||||
|
|
||||||
|
@ -3105,17 +3103,15 @@ class PlaybackManager {
|
||||||
const newPlayer = nextItem ? getPlayer(nextItem.item, nextItemPlayOptions) : null;
|
const newPlayer = nextItem ? getPlayer(nextItem.item, nextItemPlayOptions) : null;
|
||||||
|
|
||||||
if (newPlayer !== player) {
|
if (newPlayer !== player) {
|
||||||
|
data.streamInfo = null;
|
||||||
destroyPlayer(player);
|
destroyPlayer(player);
|
||||||
removeCurrentPlayer(player);
|
removeCurrentPlayer(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayErrorCode && typeof (displayErrorCode) === 'string') {
|
if (errorOccurred) {
|
||||||
showPlaybackInfoErrorMessage(self, 'PlaybackError' + displayErrorCode);
|
showPlaybackInfoErrorMessage(self, 'PlaybackError' + displayErrorCode);
|
||||||
} else if (nextItem) {
|
} else if (nextItem) {
|
||||||
self.nextTrack();
|
self.nextTrack();
|
||||||
} else {
|
|
||||||
// Nothing more to play - clear data
|
|
||||||
data.streamInfo = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3503,7 +3499,7 @@ class PlaybackManager {
|
||||||
this.seek(ticks, player);
|
this.seek(ticks, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
playTrailers(item) {
|
async playTrailers(item) {
|
||||||
const player = this._currentPlayer;
|
const player = this._currentPlayer;
|
||||||
|
|
||||||
if (player && player.playTrailers) {
|
if (player && player.playTrailers) {
|
||||||
|
@ -3512,23 +3508,14 @@ class PlaybackManager {
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||||
|
|
||||||
const instance = this;
|
let items;
|
||||||
|
|
||||||
if (item.LocalTrailerCount) {
|
if (item.LocalTrailerCount) {
|
||||||
return apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(function (result) {
|
items = await apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id);
|
||||||
return instance.play({
|
|
||||||
items: result
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const remoteTrailers = item.RemoteTrailers || [];
|
|
||||||
|
|
||||||
if (!remoteTrailers.length) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.play({
|
if (!items || !items.length) {
|
||||||
items: remoteTrailers.map(function (t) {
|
items = (item.RemoteTrailers || []).map((t) => {
|
||||||
return {
|
return {
|
||||||
Name: t.Name || (item.Name + ' Trailer'),
|
Name: t.Name || (item.Name + ' Trailer'),
|
||||||
Url: t.Url,
|
Url: t.Url,
|
||||||
|
@ -3536,9 +3523,16 @@ class PlaybackManager {
|
||||||
Type: 'Trailer',
|
Type: 'Trailer',
|
||||||
ServerId: apiClient.serverId()
|
ServerId: apiClient.serverId()
|
||||||
};
|
};
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
return this.play({
|
||||||
|
items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubtitleUrl(textStream, serverId) {
|
getSubtitleUrl(textStream, serverId) {
|
||||||
|
@ -3606,6 +3600,9 @@ class PlaybackManager {
|
||||||
setPlaybackRate(value, player = this._currentPlayer) {
|
setPlaybackRate(value, player = this._currentPlayer) {
|
||||||
if (player && player.setPlaybackRate) {
|
if (player && player.setPlaybackRate) {
|
||||||
player.setPlaybackRate(value);
|
player.setPlaybackRate(value);
|
||||||
|
|
||||||
|
// Save the new playback rate in the browser session, to restore when playing a new video.
|
||||||
|
sessionStorage.setItem('playbackRateSpeed', value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,15 +157,6 @@ import template from './playbackSettings.template.html';
|
||||||
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
|
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// hide cinema mode options if disabled at server level
|
|
||||||
apiClient.getNamedConfiguration('cinemamode').then(cinemaConfig => {
|
|
||||||
if (cinemaConfig.EnableIntrosForMovies || cinemaConfig.EnableIntrosForEpisodes) {
|
|
||||||
context.querySelector('.cinemaModeOptions').classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
context.querySelector('.cinemaModeOptions').classList.add('hide');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
|
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
|
||||||
context.querySelector('.fldExternalPlayer').classList.remove('hide');
|
context.querySelector('.fldExternalPlayer').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,6 +20,8 @@ import ServerConnections from '../ServerConnections';
|
||||||
import { playbackManager } from '../playback/playbackmanager';
|
import { playbackManager } from '../playback/playbackmanager';
|
||||||
import template from './recordingcreator.template.html';
|
import template from './recordingcreator.template.html';
|
||||||
|
|
||||||
|
import PlaceholderImage from './empty.png';
|
||||||
|
|
||||||
let currentDialog;
|
let currentDialog;
|
||||||
let closeAction;
|
let closeAction;
|
||||||
let currentRecordingFields;
|
let currentRecordingFields;
|
||||||
|
@ -70,7 +72,7 @@ function renderRecording(context, defaultTimer, program, apiClient, refreshRecor
|
||||||
const imageContainer = context.querySelector('.recordingDialog-imageContainer');
|
const imageContainer = context.querySelector('.recordingDialog-imageContainer');
|
||||||
|
|
||||||
if (imgUrl) {
|
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');
|
imageContainer.classList.remove('hide');
|
||||||
|
|
||||||
imageLoader.lazyChildren(imageContainer);
|
imageLoader.lazyChildren(imageContainer);
|
||||||
|
|
|
@ -24,9 +24,9 @@ function getEditorHtml() {
|
||||||
|
|
||||||
html += '<div class="fldSelectPlaylist selectContainer">';
|
html += '<div class="fldSelectPlaylist selectContainer">';
|
||||||
html += '<select is="emby-select" id="selectMetadataRefreshMode" label="' + globalize.translate('LabelRefreshMode') + '">';
|
html += '<select is="emby-select" id="selectMetadataRefreshMode" label="' + globalize.translate('LabelRefreshMode') + '">';
|
||||||
html += '<option value="scan">' + globalize.translate('ScanForNewAndUpdatedFiles') + '</option>';
|
html += '<option value="scan" selected>' + globalize.translate('ScanForNewAndUpdatedFiles') + '</option>';
|
||||||
html += '<option value="missing">' + globalize.translate('SearchForMissingMetadata') + '</option>';
|
html += '<option value="missing">' + globalize.translate('SearchForMissingMetadata') + '</option>';
|
||||||
html += '<option value="all" selected>' + globalize.translate('ReplaceAllMetadata') + '</option>';
|
html += '<option value="all">' + globalize.translate('ReplaceAllMetadata') + '</option>';
|
||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
|
|
|
@ -718,11 +718,9 @@ export default function () {
|
||||||
btnCommand[i].addEventListener('click', onBtnCommandClick);
|
btnCommand[i].addEventListener('click', onBtnCommandClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.querySelector('.btnToggleFullscreen').addEventListener('click', function (e) {
|
context.querySelector('.btnToggleFullscreen').addEventListener('click', function () {
|
||||||
if (currentPlayer) {
|
if (currentPlayer) {
|
||||||
playbackManager.sendCommand({
|
playbackManager.toggleFullscreen(currentPlayer);
|
||||||
Name: e.target.getAttribute('data-command')
|
|
||||||
}, currentPlayer);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
context.querySelector('.btnAudioTracks').addEventListener('click', function (e) {
|
context.querySelector('.btnAudioTracks').addEventListener('click', function (e) {
|
||||||
|
|
|
@ -172,6 +172,8 @@ export default function (options) {
|
||||||
|
|
||||||
html += '<div class="topActionButtons">';
|
html += '<div class="topActionButtons">';
|
||||||
if (actionButtonsOnTop) {
|
if (actionButtonsOnTop) {
|
||||||
|
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true);
|
||||||
|
|
||||||
if (appHost.supports('filedownload') && options.user && options.user.Policy.EnableContentDownloading) {
|
if (appHost.supports('filedownload') && options.user && options.user.Policy.EnableContentDownloading) {
|
||||||
html += getIcon('file_download', 'btnDownload slideshowButton', true);
|
html += getIcon('file_download', 'btnDownload slideshowButton', true);
|
||||||
}
|
}
|
||||||
|
@ -347,7 +349,7 @@ export default function (options) {
|
||||||
minRatio: 1,
|
minRatio: 1,
|
||||||
toggle: true
|
toggle: true
|
||||||
},
|
},
|
||||||
autoplay: !options.interactive,
|
autoplay: !options.interactive || !!options.autoplay,
|
||||||
keyboard: {
|
keyboard: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
@ -376,6 +378,8 @@ export default function (options) {
|
||||||
if (useFakeZoomImage) {
|
if (useFakeZoomImage) {
|
||||||
swiperInstance.on('zoomChange', onZoomChange);
|
swiperInstance.on('zoomChange', onZoomChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (swiperInstance.autoplay?.running) onAutoplayStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
<option value="typewriter">${Typewriter}</option>
|
<option value="typewriter">${Typewriter}</option>
|
||||||
<option value="print">${Print}</option>
|
<option value="print">${Print}</option>
|
||||||
<option value="console">${Console}</option>
|
<option value="console">${Console}</option>
|
||||||
|
<option value="cursive">${Cursive}</option>
|
||||||
<option value="casual">${Casual}</option>
|
<option value="casual">${Casual}</option>
|
||||||
<option value="smallcaps">${SmallCaps}</option>
|
<option value="smallcaps">${SmallCaps}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -193,10 +193,19 @@ class Manager {
|
||||||
this.queueCore.updatePlayQueue(apiClient, cmd.Data);
|
this.queueCore.updatePlayQueue(apiClient, cmd.Data);
|
||||||
break;
|
break;
|
||||||
case 'UserJoined':
|
case 'UserJoined':
|
||||||
|
|
||||||
toast(globalize.translate('MessageSyncPlayUserJoined', cmd.Data));
|
toast(globalize.translate('MessageSyncPlayUserJoined', cmd.Data));
|
||||||
|
if (!this.groupInfo.Participants) {
|
||||||
|
this.groupInfo.Participants = [cmd.Data];
|
||||||
|
} else {
|
||||||
|
this.groupInfo.Participants.push(cmd.Data);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'UserLeft':
|
case 'UserLeft':
|
||||||
toast(globalize.translate('MessageSyncPlayUserLeft', cmd.Data));
|
toast(globalize.translate('MessageSyncPlayUserLeft', cmd.Data));
|
||||||
|
if (this.groupInfo.Participants) {
|
||||||
|
this.groupInfo.Participants = this.groupInfo.Participants.filter((user) => user !== cmd.Data);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'GroupJoined':
|
case 'GroupJoined':
|
||||||
cmd.Data.LastUpdatedAt = new Date(cmd.Data.LastUpdatedAt);
|
cmd.Data.LastUpdatedAt = new Date(cmd.Data.LastUpdatedAt);
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
* Module that manages the playback of SyncPlay.
|
* Module that manages the playback of SyncPlay.
|
||||||
* @module components/syncPlay/core/PlaybackCore
|
* @module components/syncPlay/core/PlaybackCore
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Events } from 'jellyfin-apiclient';
|
import { Events } from 'jellyfin-apiclient';
|
||||||
|
|
||||||
|
import browser from '../../../scripts/browser';
|
||||||
import { toBoolean, toFloat } from '../../../scripts/stringUtils';
|
import { toBoolean, toFloat } from '../../../scripts/stringUtils';
|
||||||
import * as Helper from './Helper';
|
import * as Helper from './Helper';
|
||||||
import { getSetting } from './Settings';
|
import { getSetting } from './Settings';
|
||||||
|
@ -20,7 +21,6 @@ class PlaybackCore {
|
||||||
this.playbackDiffMillis = 0; // Used for stats and remote time sync.
|
this.playbackDiffMillis = 0; // Used for stats and remote time sync.
|
||||||
this.syncAttempts = 0;
|
this.syncAttempts = 0;
|
||||||
this.lastSyncTime = new Date();
|
this.lastSyncTime = new Date();
|
||||||
this.enableSyncCorrection = true; // User setting to disable sync during playback.
|
|
||||||
|
|
||||||
this.playerIsBuffering = false;
|
this.playerIsBuffering = false;
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class PlaybackCore {
|
||||||
this.useSkipToSync = toBoolean(getSetting('useSkipToSync'), true);
|
this.useSkipToSync = toBoolean(getSetting('useSkipToSync'), true);
|
||||||
|
|
||||||
// Whether sync correction during playback is active.
|
// Whether sync correction during playback is active.
|
||||||
this.enableSyncCorrection = toBoolean(getSetting('enableSyncCorrection'), true);
|
this.enableSyncCorrection = toBoolean(getSetting('enableSyncCorrection'), !(browser.mobile || browser.iOS));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -48,7 +48,7 @@ class TimeSyncCore {
|
||||||
Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]);
|
Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]);
|
||||||
});
|
});
|
||||||
|
|
||||||
Events.on(appSettings, 'change', function (e, name) {
|
Events.on(appSettings, 'change', (e, name) => {
|
||||||
if (name === 'extraTimeOffset') {
|
if (name === 'extraTimeOffset') {
|
||||||
this.extraTimeOffset = toFloat(getSetting('extraTimeOffset'), 0.0);
|
this.extraTimeOffset = toFloat(getSetting('extraTimeOffset'), 0.0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import actionsheet from '../../actionSheet/actionSheet';
|
||||||
import globalize from '../../../scripts/globalize';
|
import globalize from '../../../scripts/globalize';
|
||||||
import playbackPermissionManager from './playbackPermissionManager';
|
import playbackPermissionManager from './playbackPermissionManager';
|
||||||
import ServerConnections from '../../ServerConnections';
|
import ServerConnections from '../../ServerConnections';
|
||||||
|
import './groupSelectionMenu.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that manages the SyncPlay group selection menu.
|
* Class that manages the SyncPlay group selection menu.
|
||||||
|
@ -63,7 +64,8 @@ class GroupSelectionMenu {
|
||||||
title: globalize.translate('HeaderSyncPlaySelectGroup'),
|
title: globalize.translate('HeaderSyncPlaySelectGroup'),
|
||||||
items: menuItems,
|
items: menuItems,
|
||||||
positionTo: button,
|
positionTo: button,
|
||||||
border: true
|
border: true,
|
||||||
|
dialogClass: 'syncPlayGroupMenu'
|
||||||
};
|
};
|
||||||
|
|
||||||
actionsheet.show(menuOptions).then(function (id) {
|
actionsheet.show(menuOptions).then(function (id) {
|
||||||
|
@ -139,6 +141,8 @@ class GroupSelectionMenu {
|
||||||
|
|
||||||
const menuOptions = {
|
const menuOptions = {
|
||||||
title: groupInfo.GroupName,
|
title: groupInfo.GroupName,
|
||||||
|
text: groupInfo.Participants.join(', '),
|
||||||
|
dialogClass: 'syncPlayGroupMenu',
|
||||||
items: menuItems,
|
items: menuItems,
|
||||||
positionTo: button,
|
positionTo: button,
|
||||||
border: true
|
border: true
|
||||||
|
|
4
src/components/syncPlay/ui/groupSelectionMenu.scss
Normal file
4
src/components/syncPlay/ui/groupSelectionMenu.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.syncPlayGroupMenu .actionSheetText {
|
||||||
|
margin-left: 0.6em; /* to line up with the title */
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
|
@ -5,13 +5,12 @@
|
||||||
|
|
||||||
import { Events } from 'jellyfin-apiclient';
|
import { Events } from 'jellyfin-apiclient';
|
||||||
import SyncPlay from '../../core';
|
import SyncPlay from '../../core';
|
||||||
import { getSetting, setSetting } from '../../core/Settings';
|
import { setSetting } from '../../core/Settings';
|
||||||
import dialogHelper from '../../../dialogHelper/dialogHelper';
|
import dialogHelper from '../../../dialogHelper/dialogHelper';
|
||||||
import layoutManager from '../../../layoutManager';
|
import layoutManager from '../../../layoutManager';
|
||||||
import loading from '../../../loading/loading';
|
import loading from '../../../loading/loading';
|
||||||
import toast from '../../../toast/toast';
|
import toast from '../../../toast/toast';
|
||||||
import globalize from '../../../../scripts/globalize';
|
import globalize from '../../../../scripts/globalize';
|
||||||
import { toBoolean, toFloat } from '../../../../scripts/stringUtils';
|
|
||||||
|
|
||||||
import 'material-design-icons-iconfont';
|
import 'material-design-icons-iconfont';
|
||||||
import '../../../../elements/emby-input/emby-input';
|
import '../../../../elements/emby-input/emby-input';
|
||||||
|
@ -96,14 +95,14 @@ class SettingsEditor {
|
||||||
async initEditor() {
|
async initEditor() {
|
||||||
const { context } = this;
|
const { context } = this;
|
||||||
|
|
||||||
context.querySelector('#txtExtraTimeOffset').value = toFloat(getSetting('extraTimeOffset'), 0.0);
|
context.querySelector('#txtExtraTimeOffset').value = SyncPlay.Manager.timeSyncCore.extraTimeOffset;
|
||||||
context.querySelector('#chkSyncCorrection').checked = toBoolean(getSetting('enableSyncCorrection'), true);
|
context.querySelector('#chkSyncCorrection').checked = SyncPlay.Manager.playbackCore.enableSyncCorrection;
|
||||||
context.querySelector('#txtMinDelaySpeedToSync').value = toFloat(getSetting('minDelaySpeedToSync'), 60.0);
|
context.querySelector('#txtMinDelaySpeedToSync').value = SyncPlay.Manager.playbackCore.minDelaySpeedToSync;
|
||||||
context.querySelector('#txtMaxDelaySpeedToSync').value = toFloat(getSetting('maxDelaySpeedToSync'), 3000.0);
|
context.querySelector('#txtMaxDelaySpeedToSync').value = SyncPlay.Manager.playbackCore.maxDelaySpeedToSync;
|
||||||
context.querySelector('#txtSpeedToSyncDuration').value = toFloat(getSetting('speedToSyncDuration'), 1000.0);
|
context.querySelector('#txtSpeedToSyncDuration').value = SyncPlay.Manager.playbackCore.speedToSyncDuration;
|
||||||
context.querySelector('#txtMinDelaySkipToSync').value = toFloat(getSetting('minDelaySkipToSync'), 400.0);
|
context.querySelector('#txtMinDelaySkipToSync').value = SyncPlay.Manager.playbackCore.minDelaySkipToSync;
|
||||||
context.querySelector('#chkSpeedToSync').checked = toBoolean(getSetting('useSpeedToSync'), true);
|
context.querySelector('#chkSpeedToSync').checked = SyncPlay.Manager.playbackCore.useSpeedToSync;
|
||||||
context.querySelector('#chkSkipToSync').checked = toBoolean(getSetting('useSkipToSync'), true);
|
context.querySelector('#chkSkipToSync').checked = SyncPlay.Manager.playbackCore.useSkipToSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
|
|
@ -145,8 +145,8 @@ export default function (page, providerId, options) {
|
||||||
function onSelectPathClick(e) {
|
function onSelectPathClick(e) {
|
||||||
const page = $(e.target).parents('.xmltvForm')[0];
|
const page = $(e.target).parents('.xmltvForm')[0];
|
||||||
|
|
||||||
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
callback: function (path) {
|
callback: function (path) {
|
||||||
|
|
|
@ -4,23 +4,29 @@
|
||||||
"themes": [
|
"themes": [
|
||||||
{
|
{
|
||||||
"name": "Apple TV",
|
"name": "Apple TV",
|
||||||
"id": "appletv"
|
"id": "appletv",
|
||||||
|
"color": "#bcbcbc"
|
||||||
}, {
|
}, {
|
||||||
"name": "Blue Radiance",
|
"name": "Blue Radiance",
|
||||||
"id": "blueradiance"
|
"id": "blueradiance",
|
||||||
|
"color": "#011432"
|
||||||
}, {
|
}, {
|
||||||
"name": "Dark",
|
"name": "Dark",
|
||||||
"id": "dark",
|
"id": "dark",
|
||||||
|
"color": "#202020",
|
||||||
"default": true
|
"default": true
|
||||||
}, {
|
}, {
|
||||||
"name": "Light",
|
"name": "Light",
|
||||||
"id": "light"
|
"id": "light",
|
||||||
|
"color": "#303030"
|
||||||
}, {
|
}, {
|
||||||
"name": "Purple Haze",
|
"name": "Purple Haze",
|
||||||
"id": "purplehaze"
|
"id": "purplehaze",
|
||||||
|
"color": "#000420"
|
||||||
}, {
|
}, {
|
||||||
"name": "WMC",
|
"name": "WMC",
|
||||||
"id": "wmc"
|
"id": "wmc",
|
||||||
|
"color": "#0c2450"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menuLinks": [],
|
"menuLinks": [],
|
||||||
|
|
|
@ -13,13 +13,12 @@
|
||||||
<select is="emby-select" id="selectVideoDecoder" label="${LabelHardwareAccelerationType}">
|
<select is="emby-select" id="selectVideoDecoder" label="${LabelHardwareAccelerationType}">
|
||||||
<option value="">${None}</option>
|
<option value="">${None}</option>
|
||||||
<option value="amf">AMD AMF</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="nvenc">Nvidia NVENC</option>
|
||||||
|
<option value="qsv">Intel QuickSync (QSV)</option>
|
||||||
<option value="vaapi">Video Acceleration API (VAAPI)</option>
|
<option value="vaapi">Video Acceleration API (VAAPI)</option>
|
||||||
<option value="h264_v4l2m2m">Exynos V4L2 MFC</option>
|
<option value="videotoolbox">Apple VideoToolBox</option>
|
||||||
<option value="videotoolbox">Video ToolBox</option>
|
<option value="v4l2m2m">Video4Linux2 (V4L2)</option>
|
||||||
|
<option value="omx">OpenMAX OMX</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="fieldDescription">
|
<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>
|
<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,58 +30,54 @@
|
||||||
<div class="fieldDescription">${LabelVaapiDeviceHelp}</div>
|
<div class="fieldDescription">${LabelVaapiDeviceHelp}</div>
|
||||||
</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="hardwareAccelerationOptions hide">
|
||||||
<div class="checkboxListContainer decodingCodecsList">
|
<div class="checkboxListContainer decodingCodecsList">
|
||||||
<h3 class="checkboxListLabel">${LabelEnableHardwareDecodingFor}</h3>
|
<h3 class="checkboxListLabel">${LabelEnableHardwareDecodingFor}</h3>
|
||||||
<div class="checkboxList">
|
<div class="checkboxList">
|
||||||
<label>
|
<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>
|
<span>H264</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>HEVC</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>MPEG2</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>MPEG4</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>VC1</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>VP8</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span>VP9</span>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
<div class="checkboxList hide fld10bitHevcVp9HwDecoding">
|
||||||
|
|
||||||
<div class="checkboxListContainer">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Hevc" />
|
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Hevc" />
|
||||||
<span>${EnableDecodingColorDepth10Hevc}</span>
|
<span>HEVC 10bit</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div class="checkboxListContainer">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Vp9" />
|
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Vp9" />
|
||||||
<span>${EnableDecodingColorDepth10Vp9}</span>
|
<span>VP9 10bit</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxListContainer hide fldEnhancedNvdec">
|
<div class="checkboxListContainer hide fldEnhancedNvdec">
|
||||||
<label>
|
<label>
|
||||||
|
@ -91,13 +86,34 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxListContainer hide fldSysNativeHwDecoder">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" id="chkSystemNativeHwDecoder" />
|
||||||
|
<span>${PreferSystemNativeHwDecoder}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxListContainer">
|
<div class="checkboxListContainer">
|
||||||
|
<h3 class="checkboxListLabel">${LabelHardwareEncodingOptions}</h3>
|
||||||
<div class="checkboxList">
|
<div class="checkboxList">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkHardwareEncoding" />
|
<input type="checkbox" is="emby-checkbox" id="chkHardwareEncoding" />
|
||||||
<span>${EnableHardwareEncoding}</span>
|
<span>${EnableHardwareEncoding}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -177,6 +193,14 @@
|
||||||
<option value="6">6</option>
|
<option value="6">6</option>
|
||||||
<option value="7">7</option>
|
<option value="7">7</option>
|
||||||
<option value="8">8</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>
|
<option value="0">${OptionMax}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="fieldDescription">${LabelTranscodingThreadCountHelp}</div>
|
<div class="fieldDescription">${LabelTranscodingThreadCountHelp}</div>
|
||||||
|
|
|
@ -15,6 +15,9 @@ import alert from '../../components/alert';
|
||||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
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('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||||
|
@ -28,7 +31,6 @@ import alert from '../../components/alert';
|
||||||
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
||||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||||
page.querySelector('#txtOpenclDevice').value = config.OpenclDevice || '';
|
|
||||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||||
|
@ -81,7 +83,6 @@ import alert from '../../components/alert';
|
||||||
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
||||||
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
||||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||||
config.OpenclDevice = form.querySelector('#txtOpenclDevice').value;
|
|
||||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||||
|
@ -105,6 +106,9 @@ import alert from '../../components/alert';
|
||||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').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.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||||
|
@ -182,32 +186,42 @@ import alert from '../../components/alert';
|
||||||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value == 'nvenc' || this.value == 'amf') {
|
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'videotoolbox') {
|
||||||
page.querySelector('.fldOpenclDevice').classList.remove('hide');
|
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||||
page.querySelector('#txtOpenclDevice').setAttribute('required', 'required');
|
} else {
|
||||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('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') {
|
||||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
page.querySelector('.fldOpenclDevice').classList.add('hide');
|
|
||||||
page.querySelector('#txtOpenclDevice').removeAttribute('required');
|
|
||||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
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') {
|
if (this.value == 'nvenc') {
|
||||||
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
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) {
|
if (this.value) {
|
||||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
|
@ -217,8 +231,8 @@ import alert from '../../components/alert';
|
||||||
setDecodingCodecsVisible(page, this.value);
|
setDecodingCodecsVisible(page, this.value);
|
||||||
});
|
});
|
||||||
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
callback: function (path) {
|
callback: function (path) {
|
||||||
|
@ -232,8 +246,8 @@ import alert from '../../components/alert';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
callback: function (path) {
|
callback: function (path) {
|
||||||
if (path) {
|
if (path) {
|
||||||
|
@ -249,8 +263,8 @@ import alert from '../../components/alert';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
includeDirectories: true,
|
includeDirectories: true,
|
||||||
callback: function (path) {
|
callback: function (path) {
|
||||||
|
|
|
@ -55,8 +55,8 @@ import alert from '../../components/alert';
|
||||||
const brandingConfigKey = 'branding';
|
const brandingConfigKey = 'branding';
|
||||||
export default function (view) {
|
export default function (view) {
|
||||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
callback: function (path) {
|
callback: function (path) {
|
||||||
if (path) {
|
if (path) {
|
||||||
|
@ -72,8 +72,8 @@ import alert from '../../components/alert';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
path: $('#txtMetadataPath', view).val(),
|
path: $('#txtMetadataPath', view).val(),
|
||||||
networkSharePath: $('#txtMetadataNetworkPath', view).val(),
|
networkSharePath: $('#txtMetadataNetworkPath', view).val(),
|
||||||
|
|
|
@ -103,11 +103,6 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||||
id: 'edit',
|
id: 'edit',
|
||||||
icon: 'folder'
|
icon: 'folder'
|
||||||
});
|
});
|
||||||
menuItems.push({
|
|
||||||
name: globalize.translate('ButtonRemove'),
|
|
||||||
id: 'delete',
|
|
||||||
icon: 'delete'
|
|
||||||
});
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
name: globalize.translate('ButtonRename'),
|
name: globalize.translate('ButtonRename'),
|
||||||
id: 'rename',
|
id: 'rename',
|
||||||
|
@ -118,6 +113,11 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||||
id: 'refresh',
|
id: 'refresh',
|
||||||
icon: 'refresh'
|
icon: 'refresh'
|
||||||
});
|
});
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('ButtonRemove'),
|
||||||
|
id: 'delete',
|
||||||
|
icon: 'delete'
|
||||||
|
});
|
||||||
|
|
||||||
import('../../components/actionSheet/actionSheet').then((actionsheet) => {
|
import('../../components/actionSheet/actionSheet').then((actionsheet) => {
|
||||||
actionsheet.show({
|
actionsheet.show({
|
||||||
|
|
|
@ -67,10 +67,10 @@ import alert from '../../components/alert';
|
||||||
config.EnableIPV6 = form.querySelector('#chkEnableIP6').checked;
|
config.EnableIPV6 = form.querySelector('#chkEnableIP6').checked;
|
||||||
config.EnableIPV4 = form.querySelector('#chkEnableIP4').checked;
|
config.EnableIPV4 = form.querySelector('#chkEnableIP4').checked;
|
||||||
config.UPnPCreateHttpPortMap = form.querySelector('#chkCreateHttpPortMap').checked;
|
config.UPnPCreateHttpPortMap = form.querySelector('#chkCreateHttpPortMap').checked;
|
||||||
config.UDPPortRange = form.querySelector('#txtUDPPortRange').value || null;
|
config.UDPPortRange = form.querySelector('#txtUDPPortRange').value;
|
||||||
config.HDHomerunPortRange = form.querySelector('#txtHDHomerunPortRange').checked || null;
|
config.HDHomerunPortRange = form.querySelector('#txtHDHomerunPortRange').value;
|
||||||
config.EnableSSDPTracing = form.querySelector('#chkEnableSSDPTracing').checked;
|
config.EnableSSDPTracing = form.querySelector('#chkEnableSSDPTracing').checked;
|
||||||
config.SSDPTracingFilter = form.querySelector('#txtSSDPTracingFilter').value || null;
|
config.SSDPTracingFilter = form.querySelector('#txtSSDPTracingFilter').value;
|
||||||
ApiClient.updateNamedConfiguration('network', config).then(Dashboard.processServerConfigurationUpdateResult, Dashboard.processErrorResponse);
|
ApiClient.updateNamedConfiguration('network', config).then(Dashboard.processServerConfigurationUpdateResult, Dashboard.processErrorResponse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -157,10 +157,10 @@ import alert from '../../components/alert';
|
||||||
page.querySelector('#chkEnableIP6').checked = config.EnableIPV6;
|
page.querySelector('#chkEnableIP6').checked = config.EnableIPV6;
|
||||||
page.querySelector('#chkEnableIP4').checked = config.EnableIPV4;
|
page.querySelector('#chkEnableIP4').checked = config.EnableIPV4;
|
||||||
page.querySelector('#chkCreateHttpPortMap').checked = config.UPnPCreateHttpPortMap;
|
page.querySelector('#chkCreateHttpPortMap').checked = config.UPnPCreateHttpPortMap;
|
||||||
page.querySelector('#txtUDPPortRange').value = config.UDPPortRange;
|
page.querySelector('#txtUDPPortRange').value = config.UDPPortRange || '';
|
||||||
page.querySelector('#txtHDHomerunPortRange').checked = config.HDHomerunPortRange;
|
page.querySelector('#txtHDHomerunPortRange').checked = config.HDHomerunPortRange || '';
|
||||||
page.querySelector('#chkEnableSSDPTracing').checked = config.EnableSSDPTracing;
|
page.querySelector('#chkEnableSSDPTracing').checked = config.EnableSSDPTracing;
|
||||||
page.querySelector('#txtSSDPTracingFilter').value = config.SSDPTracingFilter;
|
page.querySelector('#txtSSDPTracingFilter').value = config.SSDPTracingFilter || '';
|
||||||
page.querySelector('#txtPublishedServer').value = (config.PublishedServerUriBySubnet || []).join(', ');
|
page.querySelector('#txtPublishedServer').value = (config.PublishedServerUriBySubnet || []).join(', ');
|
||||||
loading.hide();
|
loading.hide();
|
||||||
}
|
}
|
||||||
|
@ -181,8 +181,8 @@ import alert from '../../components/alert';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
|
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
|
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||||
const picker = new directoryBrowser();
|
const picker = new DirectoryBrowser();
|
||||||
picker.show({
|
picker.show({
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
includeDirectories: true,
|
includeDirectories: true,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
import marked from 'marked';
|
import { marked } from 'marked';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import loading from '../../../../components/loading/loading';
|
import loading from '../../../../components/loading/loading';
|
||||||
import globalize from '../../../../scripts/globalize';
|
import globalize from '../../../../scripts/globalize';
|
||||||
|
|
|
@ -70,7 +70,8 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.HasImage) {
|
if (plugin.HasImage) {
|
||||||
html += `<img src="/Plugins/${plugin.Id}/${plugin.Version}/Image" style="width:100%" />`;
|
const imageUrl = ApiClient.getUrl(`/Plugins/${plugin.Id}/${plugin.Version}/Image`);
|
||||||
|
html += `<img src="${imageUrl}" style="width:100%" />`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`;
|
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||||
html += '<span class="cardImageIcon material-icons extension"></span>';
|
html += '<span class="cardImageIcon material-icons extension"></span>';
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
<button is="emby-button" type="button" class="fab btnNewRepository submit" style="margin-left:1em;" title="${Add}">
|
<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>
|
<span class="material-icons add" aria-hidden="true"></span>
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div id="repositories"></div>
|
<div id="repositories"></div>
|
||||||
|
|
|
@ -64,7 +64,7 @@ function getRepositoryHtml(repository) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
html += '<div class="listItem listItem-border">';
|
html += '<div class="listItem listItem-border">';
|
||||||
html += `<a is="emby-linkbutton" style="margin:0;padding:0" class="clearLink listItemIconContainer" href="${repository.Url}">`;
|
html += `<a is="emby-linkbutton" style="margin:0;padding:0" class="clearLink listItemIconContainer" href="${repository.Url}" rel="noopener noreferrer" target="_blank">`;
|
||||||
html += '<span class="material-icons listItemIcon open_in_new"></span>';
|
html += '<span class="material-icons listItemIcon open_in_new"></span>';
|
||||||
html += '</a>';
|
html += '</a>';
|
||||||
html += '<div class="listItemBody two-line">';
|
html += '<div class="listItemBody two-line">';
|
||||||
|
|
|
@ -1,194 +1,3 @@
|
||||||
<div id="editUserPage" data-role="page" class="page type-interior">
|
<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>
|
</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 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>
|
</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,62 +1,3 @@
|
||||||
<div id="newUserPage" data-role="page" class="page type-interior">
|
<div id="newUserPage" data-role="page" class="page type-interior">
|
||||||
<div>
|
|
||||||
<div class="content-primary">
|
|
||||||
<form class="newUserProfileForm">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<div class="sectionTitleContainer flex align-items-center">
|
|
||||||
<h2 class="sectionTitle">${ButtonAddUser}</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 class="inputContainer">
|
|
||||||
<input is="emby-input" id="txtUsername" required type="text" label="${LabelName}" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" id="txtPassword" type="password" label="${LabelPassword}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="folderAccessContainer verticalSection">
|
|
||||||
<h2 class="sectionTitle">${HeaderLibraryAccess}</h2>
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
|
|
||||||
<span>${OptionEnableAccessToAllLibraries}</span>
|
|
||||||
</label>
|
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LibraryAccessHelp}</div>
|
|
||||||
</div>
|
|
||||||
<div class="folderAccessListContainer">
|
|
||||||
<div class="folderAccess">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="channelAccessContainer verticalSection verticalSection-extrabottompadding" style="display:none;">
|
|
||||||
<h2 class="sectionTitle">${HeaderChannelAccess}</h2>
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
|
|
||||||
<span>${OptionEnableAccessToAllChannels}</span>
|
|
||||||
</label>
|
|
||||||
<div class="fieldDescription checkboxFieldDescription">${ChannelAccessHelp}</div>
|
|
||||||
</div>
|
|
||||||
<div class="channelAccessListContainer">
|
|
||||||
<div class="channelAccess">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
import 'jquery';
|
|
||||||
import loading from '../../../components/loading/loading';
|
|
||||||
import globalize from '../../../scripts/globalize';
|
|
||||||
import '../../../elements/emby-checkbox/emby-checkbox';
|
|
||||||
import Dashboard from '../../../scripts/clientUtils';
|
|
||||||
import toast from '../../../components/toast/toast';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
function loadMediaFolders(page, mediaFolders) {
|
|
||||||
let html = '';
|
|
||||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
|
|
||||||
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
|
|
||||||
|
|
||||||
for (let i = 0; i < mediaFolders.length; i++) {
|
|
||||||
const folder = mediaFolders[i];
|
|
||||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
$('.folderAccess', page).html(html).trigger('create');
|
|
||||||
$('#chkEnableAllFolders', page).prop('checked', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadChannels(page, channels) {
|
|
||||||
let html = '';
|
|
||||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
|
|
||||||
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
|
|
||||||
|
|
||||||
for (let i = 0; i < channels.length; i++) {
|
|
||||||
const folder = channels[i];
|
|
||||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
$('.channelAccess', page).show().html(html).trigger('create');
|
|
||||||
|
|
||||||
if (channels.length) {
|
|
||||||
$('.channelAccessContainer', page).show();
|
|
||||||
} else {
|
|
||||||
$('.channelAccessContainer', page).hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#chkEnableAllChannels', page).prop('checked', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadUser(page) {
|
|
||||||
$('#txtUsername', page).val('');
|
|
||||||
$('#txtPassword', page).val('');
|
|
||||||
loading.show();
|
|
||||||
const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
|
||||||
IsHidden: false
|
|
||||||
}));
|
|
||||||
const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels'));
|
|
||||||
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
|
||||||
loadMediaFolders(page, responses[0].Items);
|
|
||||||
loadChannels(page, responses[1].Items);
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveUser(page) {
|
|
||||||
const user = {};
|
|
||||||
user.Name = $('#txtUsername', page).val();
|
|
||||||
user.Password = $('#txtPassword', page).val();
|
|
||||||
ApiClient.createUser(user).then(function (user) {
|
|
||||||
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
|
|
||||||
user.Policy.EnabledFolders = [];
|
|
||||||
|
|
||||||
if (!user.Policy.EnableAllFolders) {
|
|
||||||
user.Policy.EnabledFolders = $('.chkFolder', page).get().filter(function (i) {
|
|
||||||
return i.checked;
|
|
||||||
}).map(function (i) {
|
|
||||||
return i.getAttribute('data-id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
|
|
||||||
user.Policy.EnabledChannels = [];
|
|
||||||
|
|
||||||
if (!user.Policy.EnableAllChannels) {
|
|
||||||
user.Policy.EnabledChannels = $('.chkChannel', page).get().filter(function (i) {
|
|
||||||
return i.checked;
|
|
||||||
}).map(function (i) {
|
|
||||||
return i.getAttribute('data-id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
|
||||||
Dashboard.navigate('useredit.html?userId=' + user.Id);
|
|
||||||
});
|
|
||||||
}, function () {
|
|
||||||
toast(globalize.translate('ErrorDefault'));
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit() {
|
|
||||||
const page = $(this).parents('.page')[0];
|
|
||||||
loading.show();
|
|
||||||
saveUser(page);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadData(page) {
|
|
||||||
loadUser(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).on('pageinit', '#newUserPage', function () {
|
|
||||||
const page = this;
|
|
||||||
$('#chkEnableAllChannels', page).on('change', function () {
|
|
||||||
if (this.checked) {
|
|
||||||
$('.channelAccessListContainer', page).hide();
|
|
||||||
} else {
|
|
||||||
$('.channelAccessListContainer', page).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#chkEnableAllFolders', page).on('change', function () {
|
|
||||||
if (this.checked) {
|
|
||||||
$('.folderAccessListContainer', page).hide();
|
|
||||||
} else {
|
|
||||||
$('.folderAccessListContainer', page).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('.newUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
|
|
||||||
}).on('pageshow', '#newUserPage', function () {
|
|
||||||
loadData(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,16 +1,3 @@
|
||||||
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
|
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
|
||||||
<div>
|
|
||||||
<div class="content-primary">
|
|
||||||
<div class="verticalSection verticalSection-extrabottompadding">
|
|
||||||
<div class="sectionTitleContainer sectionTitleContainer-cards">
|
|
||||||
<h2 class="sectionTitle sectionTitle-cards">${HeaderUsers}</h2>
|
|
||||||
<button is="emby-button" type="button" class="fab btnAddUser submit sectionTitleButton" style="margin-left:1em;" title="${ButtonAddUser}">
|
|
||||||
<span class="material-icons add"></span>
|
|
||||||
</button>
|
|
||||||
<a is="emby-linkbutton" rel="noopener noreferrer" style="margin-left:2em!important;" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/users/adding-managing-users.html">${Help}</a>
|
|
||||||
</div>
|
|
||||||
<div class="localUsers itemsContainer vertical-wrap"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
import loading from '../../../components/loading/loading';
|
|
||||||
import dom from '../../../scripts/dom';
|
|
||||||
import globalize from '../../../scripts/globalize';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
|
||||||
import { localeWithSuffix } from '../../../scripts/dfnshelper';
|
|
||||||
import '../../../elements/emby-button/paper-icon-button-light';
|
|
||||||
import '../../../components/cardbuilder/card.scss';
|
|
||||||
import '../../../elements/emby-button/emby-button';
|
|
||||||
import '../../../components/indicators/indicators.scss';
|
|
||||||
import '../../../assets/css/flexstyles.scss';
|
|
||||||
import Dashboard, { pageIdOn } from '../../../scripts/clientUtils';
|
|
||||||
import confirm from '../../../components/confirm/confirm';
|
|
||||||
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
function deleteUser(page, id) {
|
|
||||||
const msg = globalize.translate('DeleteUserConfirmation');
|
|
||||||
|
|
||||||
confirm({
|
|
||||||
title: globalize.translate('DeleteUser'),
|
|
||||||
text: msg,
|
|
||||||
confirmText: globalize.translate('Delete'),
|
|
||||||
primary: 'delete'
|
|
||||||
}).then(function () {
|
|
||||||
loading.show();
|
|
||||||
ApiClient.deleteUser(id).then(function () {
|
|
||||||
loadData(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showUserMenu(elem) {
|
|
||||||
const card = dom.parentWithClass(elem, 'card');
|
|
||||||
const page = dom.parentWithClass(card, 'page');
|
|
||||||
const userId = card.getAttribute('data-userid');
|
|
||||||
const menuItems = [];
|
|
||||||
menuItems.push({
|
|
||||||
name: globalize.translate('ButtonOpen'),
|
|
||||||
id: 'open',
|
|
||||||
icon: 'mode_edit'
|
|
||||||
});
|
|
||||||
menuItems.push({
|
|
||||||
name: globalize.translate('ButtonLibraryAccess'),
|
|
||||||
id: 'access',
|
|
||||||
icon: 'lock'
|
|
||||||
});
|
|
||||||
menuItems.push({
|
|
||||||
name: globalize.translate('ButtonParentalControl'),
|
|
||||||
id: 'parentalcontrol',
|
|
||||||
icon: 'person'
|
|
||||||
});
|
|
||||||
menuItems.push({
|
|
||||||
name: globalize.translate('Delete'),
|
|
||||||
id: 'delete',
|
|
||||||
icon: 'delete'
|
|
||||||
});
|
|
||||||
|
|
||||||
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
|
||||||
actionsheet.show({
|
|
||||||
items: menuItems,
|
|
||||||
positionTo: card,
|
|
||||||
callback: function (id) {
|
|
||||||
switch (id) {
|
|
||||||
case 'open':
|
|
||||||
Dashboard.navigate('useredit.html?userId=' + userId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'access':
|
|
||||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'parentalcontrol':
|
|
||||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
deleteUser(page, userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserHtml(user) {
|
|
||||||
let html = '';
|
|
||||||
let cssClass = 'card squareCard scalableCard squareCard-scalable';
|
|
||||||
|
|
||||||
if (user.Policy.IsDisabled) {
|
|
||||||
cssClass += ' grayscale';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += "<div data-userid='" + user.Id + "' class='" + cssClass + "'>";
|
|
||||||
html += '<div class="cardBox visualCardBox">';
|
|
||||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
|
||||||
html += '<div class="cardPadder cardPadder-square"></div>';
|
|
||||||
html += `<a is="emby-linkbutton" class="cardContent ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" href="#!/useredit.html?userId=${user.Id}">`;
|
|
||||||
let imgUrl;
|
|
||||||
|
|
||||||
if (user.PrimaryImageTag) {
|
|
||||||
imgUrl = ApiClient.getUserImageUrl(user.Id, {
|
|
||||||
width: 300,
|
|
||||||
tag: user.PrimaryImageTag,
|
|
||||||
type: 'Primary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageClass = 'cardImage';
|
|
||||||
|
|
||||||
if (user.Policy.IsDisabled) {
|
|
||||||
imageClass += ' disabledUser';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imgUrl) {
|
|
||||||
html += '<div class="' + imageClass + '" style="background-image:url(\'' + imgUrl + "');\">";
|
|
||||||
} else {
|
|
||||||
html += `<div class="${imageClass} ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()} flex align-items-center justify-content-center">`;
|
|
||||||
html += '<span class="material-icons cardImageIcon person"></span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
html += '</a>';
|
|
||||||
html += '</div>';
|
|
||||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
|
||||||
html += '<div class="cardText flex align-items-center">';
|
|
||||||
html += '<div class="flex-grow" style="overflow:hidden;text-overflow:ellipsis;">';
|
|
||||||
html += user.Name;
|
|
||||||
html += '</div>';
|
|
||||||
html += '<button type="button" is="paper-icon-button-light" class="btnUserMenu flex-shrink-zero"><span class="material-icons more_vert"></span></button>';
|
|
||||||
html += '</div>';
|
|
||||||
html += '<div class="cardText cardText-secondary">';
|
|
||||||
const lastSeen = getLastSeenText(user.LastActivityDate);
|
|
||||||
html += lastSeen != '' ? lastSeen : ' ';
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
return html + '</div>';
|
|
||||||
}
|
|
||||||
// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix
|
|
||||||
// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences
|
|
||||||
function getLastSeenText(lastActivityDate) {
|
|
||||||
if (lastActivityDate) {
|
|
||||||
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserSectionHtml(users) {
|
|
||||||
return users.map(function (u__q) {
|
|
||||||
return getUserHtml(u__q);
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderUsers(page, users) {
|
|
||||||
page.querySelector('.localUsers').innerHTML = getUserSectionHtml(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadData(page) {
|
|
||||||
loading.show();
|
|
||||||
ApiClient.getUsers().then(function (users) {
|
|
||||||
renderUsers(page, users);
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pageIdOn('pageinit', 'userProfilesPage', function () {
|
|
||||||
const page = this;
|
|
||||||
page.querySelector('.btnAddUser').addEventListener('click', function() {
|
|
||||||
Dashboard.navigate('usernew.html');
|
|
||||||
});
|
|
||||||
page.querySelector('.localUsers').addEventListener('click', function (e__e) {
|
|
||||||
const btnUserMenu = dom.parentWithClass(e__e.target, 'btnUserMenu');
|
|
||||||
|
|
||||||
if (btnUserMenu) {
|
|
||||||
showUserMenu(btnUserMenu);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pageIdOn('pagebeforeshow', 'userProfilesPage', function () {
|
|
||||||
loadData(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { intervalToDuration } from 'date-fns';
|
||||||
import { appHost } from '../../components/apphost';
|
import { appHost } from '../../components/apphost';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from '../../components/loading/loading';
|
||||||
import { appRouter } from '../../components/appRouter';
|
import { appRouter } from '../../components/appRouter';
|
||||||
|
@ -668,10 +669,16 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||||
|
|
||||||
if (item.Type == 'Person' && item.PremiereDate) {
|
if (item.Type == 'Person' && item.PremiereDate) {
|
||||||
try {
|
try {
|
||||||
const birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString();
|
const birthday = datetime.parseISO8601Date(item.PremiereDate, true);
|
||||||
|
const durationSinceBorn = intervalToDuration({ start: birthday, end: Date.now() });
|
||||||
itemBirthday.classList.remove('hide');
|
itemBirthday.classList.remove('hide');
|
||||||
itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday);
|
if (item.EndDate) {
|
||||||
|
itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday.toLocaleDateString());
|
||||||
|
} else {
|
||||||
|
itemBirthday.innerHTML = `${globalize.translate('BirthDateValue', birthday.toLocaleDateString())} ${globalize.translate('AgeValue', durationSinceBorn.years)}`;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
itemBirthday.classList.add('hide');
|
itemBirthday.classList.add('hide');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -682,10 +689,18 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||||
|
|
||||||
if (item.Type == 'Person' && item.EndDate) {
|
if (item.Type == 'Person' && item.EndDate) {
|
||||||
try {
|
try {
|
||||||
const deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString();
|
const deathday = datetime.parseISO8601Date(item.EndDate, true);
|
||||||
itemDeathDate.classList.remove('hide');
|
itemDeathDate.classList.remove('hide');
|
||||||
itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday);
|
if (item.PremiereDate) {
|
||||||
|
const birthday = datetime.parseISO8601Date(item.PremiereDate, true);
|
||||||
|
const durationSinceBorn = intervalToDuration({ start: birthday, end: deathday });
|
||||||
|
|
||||||
|
itemDeathDate.innerHTML = `${globalize.translate('DeathDateValue', deathday.toLocaleDateString())} ${globalize.translate('AgeValue', durationSinceBorn.years)}`;
|
||||||
|
} else {
|
||||||
|
itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday.toLocaleDateString());
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
itemDeathDate.classList.add('hide');
|
itemDeathDate.classList.add('hide');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -320,7 +320,7 @@ import { appRouter } from '../components/appRouter';
|
||||||
return apiClient.getItems(apiClient.getCurrentUserId(), modifyQueryWithFilters(instance, {
|
return apiClient.getItems(apiClient.getCurrentUserId(), modifyQueryWithFilters(instance, {
|
||||||
StartIndex: startIndex,
|
StartIndex: startIndex,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Fields: 'PrimaryImageAspectRatio,SortName,Path',
|
Fields: 'PrimaryImageAspectRatio,SortName,Path,SongCount,ChildCount',
|
||||||
ImageTypeLimit: 1,
|
ImageTypeLimit: 1,
|
||||||
ParentId: item.Id,
|
ParentId: item.Id,
|
||||||
SortBy: sortBy
|
SortBy: sortBy
|
||||||
|
@ -666,12 +666,14 @@ class ItemsView {
|
||||||
|
|
||||||
if (currentItem && !self.hasFilters) {
|
if (currentItem && !self.hasFilters) {
|
||||||
playbackManager.play({
|
playbackManager.play({
|
||||||
items: [currentItem]
|
items: [currentItem],
|
||||||
|
autoplay: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getItems(self, self.params, currentItem, null, null, 300).then(function (result) {
|
getItems(self, self.params, currentItem, null, null, 300).then(function (result) {
|
||||||
playbackManager.play({
|
playbackManager.play({
|
||||||
items: result.Items
|
items: result.Items,
|
||||||
|
autoplay: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -701,7 +703,8 @@ class ItemsView {
|
||||||
} else {
|
} else {
|
||||||
getItems(self, self.params, currentItem, 'Random', null, 300).then(function (result) {
|
getItems(self, self.params, currentItem, 'Random', null, 300).then(function (result) {
|
||||||
playbackManager.play({
|
playbackManager.play({
|
||||||
items: result.Items
|
items: result.Items,
|
||||||
|
autoplay: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -788,21 +791,37 @@ class ItemsView {
|
||||||
|
|
||||||
const itemType = item ? item.Type : null;
|
const itemType = item ? item.Type : null;
|
||||||
|
|
||||||
if (itemType === 'MusicGenre' || params.type !== 'Programs' && itemType !== 'Channel') {
|
if ((itemType === 'MusicGenre' || params.type !== 'Programs' && itemType !== 'Channel')
|
||||||
|
// Folder, Playlist views
|
||||||
|
&& itemType !== 'UserView'
|
||||||
|
// Only Photo (homevideos) CollectionFolders are supported
|
||||||
|
&& !(itemType === 'CollectionFolder' && item?.CollectionType !== 'homevideos')
|
||||||
|
) {
|
||||||
|
// Show Play All buttons
|
||||||
hideOrShowAll(view.querySelectorAll('.btnPlay'), false);
|
hideOrShowAll(view.querySelectorAll('.btnPlay'), false);
|
||||||
} else {
|
} else {
|
||||||
|
// Hide Play All buttons
|
||||||
hideOrShowAll(view.querySelectorAll('.btnPlay'), true);
|
hideOrShowAll(view.querySelectorAll('.btnPlay'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === 'MusicGenre' || params.type !== 'Programs' && params.type !== 'nextup' && itemType !== 'Channel') {
|
if ((itemType === 'MusicGenre' || params.type !== 'Programs' && params.type !== 'nextup' && itemType !== 'Channel')
|
||||||
|
// Folder, Playlist views
|
||||||
|
&& itemType !== 'UserView'
|
||||||
|
// Only Photo (homevideos) CollectionFolders are supported
|
||||||
|
&& !(itemType === 'CollectionFolder' && item?.CollectionType !== 'homevideos')
|
||||||
|
) {
|
||||||
|
// Show Shuffle buttons
|
||||||
hideOrShowAll(view.querySelectorAll('.btnShuffle'), false);
|
hideOrShowAll(view.querySelectorAll('.btnShuffle'), false);
|
||||||
} else {
|
} else {
|
||||||
|
// Hide Shuffle buttons
|
||||||
hideOrShowAll(view.querySelectorAll('.btnShuffle'), true);
|
hideOrShowAll(view.querySelectorAll('.btnShuffle'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item && playbackManager.canQueue(item)) {
|
if (item && playbackManager.canQueue(item)) {
|
||||||
|
// Show Queue button
|
||||||
hideOrShowAll(view.querySelectorAll('.btnQueue'), false);
|
hideOrShowAll(view.querySelectorAll('.btnQueue'), false);
|
||||||
} else {
|
} else {
|
||||||
|
// Hide Queue button
|
||||||
hideOrShowAll(view.querySelectorAll('.btnQueue'), true);
|
hideOrShowAll(view.querySelectorAll('.btnQueue'), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<div id="liveTvSuggestedPage" data-dom-cache="true" data-role="page" class="page libraryPage liveTvPage pageWithAbsoluteTabs withTabs" data-title="${LiveTV}" data-backdroptype="series,movie">
|
<div id="liveTvSuggestedPage" data-dom-cache="true" data-role="page" class="page libraryPage liveTvPage pageWithAbsoluteTabs withTabs" data-title="${LiveTV}" data-backdroptype="series,movie">
|
||||||
|
|
||||||
<div class="liveTvContainer">
|
<div class="liveTvContainer">
|
||||||
|
|
||||||
<div class="pageTabContent" id="suggestionsTab" data-index="0">
|
<div class="pageTabContent" id="suggestionsTab" data-index="0">
|
||||||
<div id="activePrograms" class="verticalSection">
|
<div id="activePrograms" class="verticalSection">
|
||||||
<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">
|
<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">
|
||||||
|
@ -90,8 +88,5 @@
|
||||||
<div class="pageTabContent padded-top" id="seriesTab" data-index="5">
|
<div class="pageTabContent padded-top" id="seriesTab" data-index="5">
|
||||||
<div is="emby-itemscontainer" class="vertical-wrap itemsContainer centered padded-left padded-right" id="items"></div>
|
<div is="emby-itemscontainer" class="vertical-wrap itemsContainer centered padded-left padded-right" id="items"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pageTabContent" data-index="6">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -61,7 +61,7 @@ function renderActiveRecordings(context, promise) {
|
||||||
defaultShape: getBackdropShape(),
|
defaultShape: getBackdropShape(),
|
||||||
showParentTitle: false,
|
showParentTitle: false,
|
||||||
showParentTitleOrTitle: true,
|
showParentTitleOrTitle: true,
|
||||||
showTitle: false,
|
showTitle: true,
|
||||||
showAirTime: true,
|
showAirTime: true,
|
||||||
showAirEndTime: true,
|
showAirEndTime: true,
|
||||||
showChannelName: true,
|
showChannelName: true,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue