mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into remove-dom-exception-code-property
This commit is contained in:
commit
002d994466
141 changed files with 8245 additions and 5697 deletions
1
.copr
1
.copr
|
@ -1 +0,0 @@
|
||||||
fedora/
|
|
|
@ -264,6 +264,8 @@ module.exports = {
|
||||||
'Windows': 'readonly',
|
'Windows': 'readonly',
|
||||||
// Build time definitions
|
// Build time definitions
|
||||||
__JF_BUILD_VERSION__: 'readonly',
|
__JF_BUILD_VERSION__: 'readonly',
|
||||||
|
__PACKAGE_JSON_NAME__: 'readonly',
|
||||||
|
__PACKAGE_JSON_VERSION__: 'readonly',
|
||||||
__USE_SYSTEM_FONTS__: 'readonly',
|
__USE_SYSTEM_FONTS__: 'readonly',
|
||||||
__WEBPACK_SERVE__: 'readonly'
|
__WEBPACK_SERVE__: 'readonly'
|
||||||
},
|
},
|
||||||
|
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["github>jellyfin/.github//renovate-presets/nodejs", ":semanticCommitsDisabled"]
|
"extends": [
|
||||||
|
"github>jellyfin/.github//renovate-presets/nodejs",
|
||||||
|
":semanticCommitsDisabled",
|
||||||
|
":dependencyDashboard"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
2
.github/workflows/automation.yml
vendored
2
.github/workflows/automation.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
|
- uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||||
|
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
|
@ -22,13 +22,13 @@ jobs:
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||||
|
|
2
.github/workflows/pr-suggestions.yml
vendored
2
.github/workflows/pr-suggestions.yml
vendored
|
@ -33,6 +33,6 @@ jobs:
|
||||||
|
|
||||||
- name: Run eslint
|
- name: Run eslint
|
||||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||||
uses: CatChen/eslint-suggestion-action@8fb7db4e235f7af9fc434349a124034b681d99a3 # v3.1.3
|
uses: CatChen/eslint-suggestion-action@34e2a6c4193eba18a7a20710b5ae37850fc984c3 # v3.1.5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download workflow artifact
|
- name: Download workflow artifact
|
||||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||||
with:
|
with:
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
name: jellyfin-web__prod
|
name: jellyfin-web__prod
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get PR context
|
- name: Get PR context
|
||||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||||
id: pr_context
|
id: pr_context
|
||||||
with:
|
with:
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
|
2
.github/workflows/update-sdk.yml
vendored
2
.github/workflows/update-sdk.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
|
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Open a pull request
|
- name: Open a pull request
|
||||||
uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6.0.1
|
uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6.0.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,6 +8,7 @@ config.json
|
||||||
|
|
||||||
# ide
|
# ide
|
||||||
.idea
|
.idea
|
||||||
|
.vs
|
||||||
|
|
||||||
# log
|
# log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
|
@ -79,7 +79,13 @@
|
||||||
- [Kevin Tan (Valius)](https://github.com/valius)
|
- [Kevin Tan (Valius)](https://github.com/valius)
|
||||||
- [Rasmus Krämer](https://github.com/rasmuslos)
|
- [Rasmus Krämer](https://github.com/rasmuslos)
|
||||||
- [ntarelix](https://github.com/ntarelix)
|
- [ntarelix](https://github.com/ntarelix)
|
||||||
|
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||||
- [András Maróy](https://github.com/andrasmaroy)
|
- [András Maróy](https://github.com/andrasmaroy)
|
||||||
|
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
|
||||||
|
- [Vedant](https://github.com/viktory36)
|
||||||
|
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||||
|
- [JPUC1143](https://github.com/Jpuc1143)
|
||||||
|
- [David Angel](https://github.com/davidangel)
|
||||||
|
|
||||||
## Emby Contributors
|
## Emby Contributors
|
||||||
|
|
||||||
|
|
110
build.sh
110
build.sh
|
@ -1,110 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# build.sh - Build Jellyfin binary packages
|
|
||||||
# Part of the Jellyfin Project
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo -e "build.sh - Build Jellyfin binary packages"
|
|
||||||
echo -e "Usage:"
|
|
||||||
echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
|
|
||||||
echo -e "Notes:"
|
|
||||||
echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
|
|
||||||
echo -e " * native: Build using the build script in the host OS"
|
|
||||||
echo -e " * docker: Build using the build script in a standardized Docker container"
|
|
||||||
echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
|
|
||||||
echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
|
|
||||||
echo -e " retained after the build is finished; the source directory will still be cleaned"
|
|
||||||
echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
|
|
||||||
echo -e " the list of supported platforms and exit"
|
|
||||||
}
|
|
||||||
|
|
||||||
list_platforms() {
|
|
||||||
declare -a platforms
|
|
||||||
platforms=(
|
|
||||||
$( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
|
|
||||||
)
|
|
||||||
echo -e "Valid platforms:"
|
|
||||||
echo
|
|
||||||
for platform in ${platforms[@]}; do
|
|
||||||
echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
do_build_native() {
|
|
||||||
export IS_DOCKER=NO
|
|
||||||
deployment/build.${PLATFORM}
|
|
||||||
}
|
|
||||||
|
|
||||||
do_build_docker() {
|
|
||||||
if ! [ $(uname -m) = "x86_64" ]; then
|
|
||||||
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
|
|
||||||
echo "Missing Dockerfile for platform ${PLATFORM}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ${KEEP_ARTIFACTS} == YES ]]; then
|
|
||||||
docker_args=""
|
|
||||||
else
|
|
||||||
docker_args="--rm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
key="$1"
|
|
||||||
case $key in
|
|
||||||
-t|--type)
|
|
||||||
BUILD_TYPE="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-p|--platform)
|
|
||||||
PLATFORM="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-k|--keep-artifacts)
|
|
||||||
KEEP_ARTIFACTS=YES
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-l|--list-platforms)
|
|
||||||
list_platforms
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export SOURCE_DIR="$( pwd )"
|
|
||||||
export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
|
|
||||||
|
|
||||||
# Determine build type
|
|
||||||
case ${BUILD_TYPE} in
|
|
||||||
native)
|
|
||||||
do_build_native
|
|
||||||
;;
|
|
||||||
docker)
|
|
||||||
do_build_docker
|
|
||||||
;;
|
|
||||||
esac
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
# We just wrap `build` so this is really it
|
|
||||||
name: "jellyfin-web"
|
|
||||||
version: "10.8.0"
|
|
||||||
packages:
|
|
||||||
- debian.all
|
|
||||||
- fedora.all
|
|
||||||
- centos.all
|
|
||||||
- portable
|
|
67
bump_version
67
bump_version
|
@ -7,7 +7,7 @@ set -o pipefail
|
||||||
set -o xtrace
|
set -o xtrace
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo -e "bump_version - increase the shared version and generate changelogs"
|
echo -e "bump_version - increase the shared version"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "Usage:"
|
echo -e "Usage:"
|
||||||
echo -e " $ bump_version <new_version>"
|
echo -e " $ bump_version <new_version>"
|
||||||
|
@ -18,75 +18,12 @@ if [[ -z $1 ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build_file="./build.yaml"
|
|
||||||
package_file="./package*.json"
|
|
||||||
|
|
||||||
new_version="$1"
|
new_version="$1"
|
||||||
|
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||||
old_version="$(
|
|
||||||
grep "version:" ${build_file} \
|
|
||||||
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
|
||||||
)"
|
|
||||||
echo "Old version: ${old_version}"
|
|
||||||
|
|
||||||
# Bump the NPM version
|
# Bump the NPM version
|
||||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
|
||||||
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
||||||
|
|
||||||
# Set the build.yaml version to the specified new_version
|
|
||||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
|
||||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
|
|
||||||
|
|
||||||
|
|
||||||
if [[ ${new_version} == *"-"* ]]; then
|
|
||||||
new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
|
|
||||||
new_version_deb_sup=""
|
|
||||||
else
|
|
||||||
new_version_pkg="${new_version}"
|
|
||||||
new_version_deb_sup="-1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
|
||||||
debian_changelog_file="debian/changelog"
|
|
||||||
debian_changelog_temp="$( mktemp )"
|
|
||||||
# Create new temp file with our changelog
|
|
||||||
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}
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
|
||||||
" >> ${debian_changelog_temp}
|
|
||||||
cat ${debian_changelog_file} >> ${debian_changelog_temp}
|
|
||||||
# Move into place
|
|
||||||
mv ${debian_changelog_temp} ${debian_changelog_file}
|
|
||||||
|
|
||||||
# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
|
|
||||||
fedora_spec_file="fedora/jellyfin-web.spec"
|
|
||||||
fedora_changelog_temp="$( mktemp )"
|
|
||||||
fedora_spec_temp_dir="$( mktemp -d )"
|
|
||||||
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin-web.spec.tmp"
|
|
||||||
# Make a copy of our spec file for hacking
|
|
||||||
cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
|
|
||||||
pushd ${fedora_spec_temp_dir}
|
|
||||||
# Split out the stuff before and after changelog
|
|
||||||
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
|
|
||||||
# Update the version in xx00
|
|
||||||
sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
|
|
||||||
# Remove the header from xx01
|
|
||||||
sed -i '/^%changelog/d' xx01
|
|
||||||
# Create new temp file with our changelog
|
|
||||||
echo -e "%changelog
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
|
|
||||||
cat xx01 >> ${fedora_changelog_temp}
|
|
||||||
# Reassembble
|
|
||||||
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
|
|
||||||
popd
|
|
||||||
# Move into place
|
|
||||||
mv ${fedora_spec_temp} ${fedora_spec_file}
|
|
||||||
# Clean up
|
|
||||||
rm -rf ${fedora_spec_temp_dir}
|
|
||||||
|
|
||||||
# Stage the changed files for commit
|
# Stage the changed files for commit
|
||||||
git add .
|
git add .
|
||||||
git status -v
|
git status -v
|
||||||
|
|
17
debian/changelog
vendored
17
debian/changelog
vendored
|
@ -1,17 +0,0 @@
|
||||||
jellyfin-web (10.8.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Forthcoming stable release
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:58:23 -0500
|
|
||||||
|
|
||||||
jellyfin-web (10.7.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Forthcoming stable release
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:13:31 -0400
|
|
||||||
|
|
||||||
jellyfin-web (10.6.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* New upstream version 10.6.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.0
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 16 Mar 2020 11:15:00 -0400
|
|
1
debian/compat
vendored
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
||||||
8
|
|
1
debian/conffiles
vendored
1
debian/conffiles
vendored
|
@ -1 +0,0 @@
|
||||||
/usr/share/jellyfin/web/config.json
|
|
16
debian/control
vendored
16
debian/control
vendored
|
@ -1,16 +0,0 @@
|
||||||
Source: jellyfin-web
|
|
||||||
Section: misc
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Jellyfin Team <team@jellyfin.org>
|
|
||||||
Build-Depends: debhelper (>= 9),
|
|
||||||
npm | nodejs
|
|
||||||
Standards-Version: 3.9.4
|
|
||||||
Homepage: https://jellyfin.org/
|
|
||||||
Vcs-Git: https://github.org/jellyfin/jellyfin-web.git
|
|
||||||
Vcs-Browser: https://github.org/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
Package: jellyfin-web
|
|
||||||
Recommends: jellyfin-server
|
|
||||||
Architecture: all
|
|
||||||
Description: Jellyfin is the Free Software Media System.
|
|
||||||
This package provides the Jellyfin web client.
|
|
28
debian/copyright
vendored
28
debian/copyright
vendored
|
@ -1,28 +0,0 @@
|
||||||
Format: http://dep.debian.net/deps/dep5
|
|
||||||
Upstream-Name: jellyfin-web
|
|
||||||
Source: https://github.com/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2018-2020 Jellyfin Team
|
|
||||||
License: GPL-3.0
|
|
||||||
|
|
||||||
Files: debian/*
|
|
||||||
Copyright: 2020 Joshua Boniface <joshua@boniface.me>
|
|
||||||
License: GPL-3.0
|
|
||||||
|
|
||||||
License: GPL-3.0
|
|
||||||
This package is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
.
|
|
||||||
This package is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
.
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
.
|
|
||||||
On Debian systems, the complete text of the GNU General
|
|
||||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
|
6
debian/gbp.conf
vendored
6
debian/gbp.conf
vendored
|
@ -1,6 +0,0 @@
|
||||||
[DEFAULT]
|
|
||||||
pristine-tar = False
|
|
||||||
cleaner = fakeroot debian/rules clean
|
|
||||||
|
|
||||||
[import-orig]
|
|
||||||
filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]
|
|
1
debian/install
vendored
1
debian/install
vendored
|
@ -1 +0,0 @@
|
||||||
web usr/share/jellyfin/
|
|
1
debian/po/POTFILES.in
vendored
1
debian/po/POTFILES.in
vendored
|
@ -1 +0,0 @@
|
||||||
[type: gettext/rfc822deb] templates
|
|
57
debian/po/templates.pot
vendored
57
debian/po/templates.pot
vendored
|
@ -1,57 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: jellyfin-server\n"
|
|
||||||
"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
|
|
||||||
"POT-Creation-Date: 2015-06-12 20:51-0600\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:1001
|
|
||||||
msgid "Jellyfin permission info:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:1001
|
|
||||||
msgid ""
|
|
||||||
"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
|
|
||||||
"user jellyfin has read and write access to any folders you wish to add to your "
|
|
||||||
"library. Otherwise please run jellyfin under a different user."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: string
|
|
||||||
#. Description
|
|
||||||
#: ../templates:2001
|
|
||||||
msgid "Username to run Jellyfin as:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: string
|
|
||||||
#. Description
|
|
||||||
#: ../templates:2001
|
|
||||||
msgid "The user that jellyfin will run as."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:3001
|
|
||||||
msgid "Jellyfin still running"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:3001
|
|
||||||
msgid "Jellyfin is currently running. Please close it and try again."
|
|
||||||
msgstr ""
|
|
21
debian/rules
vendored
21
debian/rules
vendored
|
@ -1,21 +0,0 @@
|
||||||
#! /usr/bin/make -f
|
|
||||||
export DH_VERBOSE=1
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
||||||
|
|
||||||
# disable "make check"
|
|
||||||
override_dh_auto_test:
|
|
||||||
|
|
||||||
# disable stripping debugging symbols
|
|
||||||
override_dh_clistrip:
|
|
||||||
|
|
||||||
override_dh_auto_build:
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
mv $(CURDIR)/dist $(CURDIR)/web
|
|
||||||
|
|
||||||
override_dh_auto_clean:
|
|
||||||
test -d $(CURDIR)/dist && rm -rf '$(CURDIR)/dist' || true
|
|
||||||
test -d $(CURDIR)/web && rm -rf '$(CURDIR)/web' || true
|
|
||||||
test -d $(CURDIR)/node_modules && rm -rf '$(CURDIR)/node_modules' || true
|
|
1
debian/source/format
vendored
1
debian/source/format
vendored
|
@ -1 +0,0 @@
|
||||||
1.0
|
|
7
debian/source/options
vendored
7
debian/source/options
vendored
|
@ -1,7 +0,0 @@
|
||||||
tar-ignore='.git*'
|
|
||||||
tar-ignore='**/.git'
|
|
||||||
tar-ignore='**/.hg'
|
|
||||||
tar-ignore='**/.vs'
|
|
||||||
tar-ignore='**/.vscode'
|
|
||||||
tar-ignore='deployment'
|
|
||||||
tar-ignore='*.deb'
|
|
|
@ -1,28 +0,0 @@
|
||||||
FROM quay.io/centos/centos:stream8
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare CentOS environment
|
|
||||||
RUN yum update -y \
|
|
||||||
&& yum install -y epel-release \
|
|
||||||
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
|
||||||
&& yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
|
||||||
&& yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1 \
|
|
||||||
&& yum clean all \
|
|
||||||
&& rm -rf /var/cache/dnf
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,30 +0,0 @@
|
||||||
FROM debian:12
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV DEB_BUILD_OPTIONS=noddebs
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Debian build environment
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y debhelper mmv git curl gnupg ca-certificates \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,13 +0,0 @@
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
ARG SOURCE_DIR=/src
|
|
||||||
ARG ARTIFACT_DIR=/jellyfin-web
|
|
||||||
|
|
||||||
RUN apk --no-cache add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3
|
|
||||||
|
|
||||||
WORKDIR ${SOURCE_DIR}
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN npm ci --no-audit --unsafe-perm \
|
|
||||||
&& npm run build:production \
|
|
||||||
&& mv dist ${ARTIFACT_DIR}
|
|
|
@ -1,26 +0,0 @@
|
||||||
FROM fedora:40
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Fedora environment
|
|
||||||
RUN dnf update -y \
|
|
||||||
&& dnf install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
|
||||||
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make --setopt=nodesource-nodejs.module_hotfixes=1 \
|
|
||||||
&& dnf clean all \
|
|
||||||
&& rm -rf /var/cache/dnf
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,29 +0,0 @@
|
||||||
FROM debian:12
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Debian build environment
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y mmv curl git gnupg ca-certificates \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd fedora
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
|
|
||||||
sed -i "/%changelog/q" jellyfin-web.spec
|
|
||||||
|
|
||||||
cat <<EOF >>jellyfin-web.spec
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build rpm
|
|
||||||
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
|
|
||||||
rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f fedora/jellyfin*.tar.gz
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd debian
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
cat <<EOF >changelog
|
|
||||||
jellyfin-web (${BUILD_ID}-unstable) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build deb
|
|
||||||
dpkg-buildpackage -us -uc --pre-clean --post-clean
|
|
||||||
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd fedora
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
|
|
||||||
sed -i "/%changelog/q" jellyfin-web.spec
|
|
||||||
|
|
||||||
cat <<EOF >>jellyfin-web.spec
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build rpm
|
|
||||||
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
|
|
||||||
rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f fedora/jellyfin*.tar.gz
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
# get version
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
version="${BUILD_ID}"
|
|
||||||
else
|
|
||||||
version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build archives
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
mv dist jellyfin-web_${version}
|
|
||||||
tar -czf jellyfin-web_${version}_portable.tar.gz jellyfin-web_${version}
|
|
||||||
rm -rf dist
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,48 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_20.x/nodistro/\$$basearch/
|
|
||||||
|
|
||||||
fed_ver := $(shell rpm -E %fedora)
|
|
||||||
# fallback when not running on Fedora
|
|
||||||
fed_ver ?= 36
|
|
||||||
TARGET ?= fedora-$(fed_ver)-x86_64
|
|
||||||
|
|
||||||
outdir ?= $(PWD)/$(DIR)/
|
|
||||||
|
|
||||||
srpm: $(DIR)/$(SRPM)
|
|
||||||
tarball: $(DIR)/$(TARBALL)
|
|
||||||
|
|
||||||
$(DIR)/$(TARBALL):
|
|
||||||
cd $(DIR)/; \
|
|
||||||
SOURCE_DIR=.. \
|
|
||||||
WORKDIR="$${PWD}"; \
|
|
||||||
version=$(VERSION); \
|
|
||||||
tar \
|
|
||||||
--transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='**/.git' \
|
|
||||||
--exclude='**/.hg' \
|
|
||||||
--exclude=deployment \
|
|
||||||
--exclude='*.deb' \
|
|
||||||
--exclude='*.rpm' \
|
|
||||||
--exclude=$(notdir $@) \
|
|
||||||
-czf $(notdir $@) \
|
|
||||||
-C $${SOURCE_DIR} ./
|
|
||||||
|
|
||||||
$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin-web.spec
|
|
||||||
cd $(DIR)/; \
|
|
||||||
rpmbuild -bs $(NAME).spec \
|
|
||||||
--define "_sourcedir $$PWD/" \
|
|
||||||
--define "_srcrpmdir $(outdir)"
|
|
||||||
|
|
||||||
rpms: $(DIR)/$(SRPM)
|
|
||||||
mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
|
|
||||||
--enable-network \
|
|
||||||
-r $(TARGET) $<
|
|
|
@ -1,58 +0,0 @@
|
||||||
%global debug_package %{nil}
|
|
||||||
|
|
||||||
Name: jellyfin-web
|
|
||||||
Version: 10.8.0
|
|
||||||
Release: 2%{?dist}
|
|
||||||
Summary: The Free Software Media System web client
|
|
||||||
License: GPLv2
|
|
||||||
URL: https://jellyfin.org
|
|
||||||
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
|
|
||||||
Source0: jellyfin-web-%{version}.tar.gz
|
|
||||||
|
|
||||||
BuildArch: noarch
|
|
||||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
|
||||||
BuildRequires: nodejs
|
|
||||||
%else
|
|
||||||
BuildRequires: git
|
|
||||||
# Nodejs 20 is required and npm >= 10 should bring in NodeJS 20
|
|
||||||
# This requires the build environment to use the nodejs:20 module stream:
|
|
||||||
# dnf module {install|switch-to}:web nodejs:20
|
|
||||||
BuildRequires: npm >= 10
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%description
|
|
||||||
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
|
|
||||||
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%autosetup -n jellyfin-web-%{version} -b 0
|
|
||||||
|
|
||||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
|
||||||
# Required for CentOS build
|
|
||||||
chown root:root -R .
|
|
||||||
%endif
|
|
||||||
|
|
||||||
|
|
||||||
%build
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
|
|
||||||
|
|
||||||
%install
|
|
||||||
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
%{__cp} -r dist/* %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
|
|
||||||
%files
|
|
||||||
%defattr(644,root,root,755)
|
|
||||||
%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
%license LICENSE
|
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
||||||
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
||||||
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
8844
package-lock.json
generated
8844
package-lock.json
generated
File diff suppressed because it is too large
Load diff
71
package.json
71
package.json
|
@ -5,80 +5,81 @@
|
||||||
"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.23.7",
|
"@babel/core": "7.24.3",
|
||||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||||
"@babel/plugin-transform-modules-umd": "7.23.3",
|
"@babel/plugin-transform-modules-umd": "7.24.1",
|
||||||
"@babel/preset-env": "7.23.8",
|
"@babel/preset-env": "7.24.3",
|
||||||
"@babel/preset-react": "7.23.3",
|
"@babel/preset-react": "7.24.1",
|
||||||
"@types/escape-html": "1.0.4",
|
"@types/escape-html": "1.0.4",
|
||||||
"@types/loadable__component": "5.13.9",
|
"@types/loadable__component": "5.13.9",
|
||||||
"@types/lodash-es": "4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/markdown-it": "13.0.7",
|
"@types/markdown-it": "13.0.7",
|
||||||
"@types/react": "17.0.75",
|
"@types/react": "17.0.79",
|
||||||
"@types/react-dom": "17.0.25",
|
"@types/react-dom": "17.0.25",
|
||||||
"@types/sortablejs": "1.15.8",
|
"@types/sortablejs": "1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
"@typescript-eslint/parser": "5.62.0",
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||||
"autoprefixer": "10.4.17",
|
"autoprefixer": "10.4.19",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.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.11",
|
"confusing-browser-globals": "1.0.11",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.9.1",
|
"css-loader": "6.10.0",
|
||||||
"cssnano": "6.0.5",
|
"cssnano": "6.1.1",
|
||||||
"es-check": "7.1.1",
|
"es-check": "7.1.1",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-compat": "4.2.0",
|
"eslint-plugin-compat": "4.2.0",
|
||||||
"eslint-plugin-eslint-comments": "3.2.0",
|
"eslint-plugin-eslint-comments": "3.2.0",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "6.8.0",
|
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||||
"eslint-plugin-react": "7.33.2",
|
"eslint-plugin-react": "7.34.1",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-sonarjs": "0.23.0",
|
"eslint-plugin-sonarjs": "0.24.0",
|
||||||
"expose-loader": "4.1.0",
|
"expose-loader": "4.1.0",
|
||||||
"fork-ts-checker-webpack-plugin": "9.0.2",
|
"fork-ts-checker-webpack-plugin": "9.0.2",
|
||||||
"html-loader": "4.2.0",
|
"html-loader": "4.2.0",
|
||||||
"html-webpack-plugin": "5.6.0",
|
"html-webpack-plugin": "5.6.0",
|
||||||
"jsdom": "23.2.0",
|
"jsdom": "23.2.0",
|
||||||
"mini-css-extract-plugin": "2.7.7",
|
"mini-css-extract-plugin": "2.8.1",
|
||||||
"postcss": "8.4.33",
|
"postcss": "8.4.38",
|
||||||
"postcss-loader": "7.3.4",
|
"postcss-loader": "7.3.4",
|
||||||
"postcss-preset-env": "9.3.0",
|
"postcss-preset-env": "9.5.2",
|
||||||
"postcss-scss": "4.0.9",
|
"postcss-scss": "4.0.9",
|
||||||
"sass": "1.70.0",
|
"sass": "1.72.0",
|
||||||
"sass-loader": "13.3.3",
|
"sass-loader": "13.3.3",
|
||||||
"source-map-loader": "4.0.2",
|
"source-map-loader": "4.0.2",
|
||||||
"speed-measure-webpack-plugin": "1.5.0",
|
"speed-measure-webpack-plugin": "1.5.0",
|
||||||
"style-loader": "3.3.4",
|
"style-loader": "3.3.4",
|
||||||
"stylelint": "15.11.0",
|
"stylelint": "15.11.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.3.0",
|
||||||
"stylelint-order": "6.0.4",
|
"stylelint-order": "6.0.4",
|
||||||
"stylelint-scss": "5.3.2",
|
"stylelint-scss": "5.3.2",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.4.3",
|
||||||
"vitest": "1.3.0",
|
"vitest": "1.4.0",
|
||||||
"webpack": "5.89.0",
|
"webpack": "5.91.0",
|
||||||
"webpack-bundle-analyzer": "4.10.1",
|
"webpack-bundle-analyzer": "4.10.1",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "4.15.1",
|
"webpack-dev-server": "4.15.2",
|
||||||
"webpack-merge": "5.10.0",
|
"webpack-merge": "5.10.0",
|
||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "11.11.4",
|
"@emotion/react": "11.11.4",
|
||||||
"@emotion/styled": "11.11.0",
|
"@emotion/styled": "11.11.0",
|
||||||
"@fontsource/noto-sans": "5.0.18",
|
"@fontsource/noto-sans": "5.0.21",
|
||||||
"@fontsource/noto-sans-hk": "5.0.17",
|
"@fontsource/noto-sans-hk": "5.0.18",
|
||||||
"@fontsource/noto-sans-jp": "5.0.17",
|
"@fontsource/noto-sans-jp": "5.0.18",
|
||||||
"@fontsource/noto-sans-kr": "5.0.17",
|
"@fontsource/noto-sans-kr": "5.0.18",
|
||||||
"@fontsource/noto-sans-sc": "5.0.17",
|
"@fontsource/noto-sans-sc": "5.0.18",
|
||||||
"@fontsource/noto-sans-tc": "5.0.17",
|
"@fontsource/noto-sans-tc": "5.0.18",
|
||||||
"@jellyfin/sdk": "0.0.0-unstable.202403100501",
|
"@jellyfin/libass-wasm": "4.2.1",
|
||||||
|
"@jellyfin/sdk": "0.0.0-unstable.202403310501",
|
||||||
"@loadable/component": "5.16.3",
|
"@loadable/component": "5.16.3",
|
||||||
"@mui/icons-material": "5.15.11",
|
"@mui/icons-material": "5.15.11",
|
||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
|
@ -91,25 +92,23 @@
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"core-js": "3.35.1",
|
"core-js": "3.36.1",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"dompurify": "3.0.1",
|
"dompurify": "3.0.1",
|
||||||
"epubjs": "0.3.93",
|
"epubjs": "0.3.93",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"event-target-polyfill": "github:ThaUnknown/event-target-polyfill",
|
|
||||||
"fast-text-encoding": "1.0.6",
|
"fast-text-encoding": "1.0.6",
|
||||||
"flv.js": "1.6.2",
|
"flv.js": "1.6.2",
|
||||||
"headroom.js": "0.12.0",
|
"headroom.js": "0.12.0",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"hls.js": "1.5.7",
|
"hls.js": "1.5.7",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"jassub": "1.7.15",
|
|
||||||
"jellyfin-apiclient": "1.11.0",
|
"jellyfin-apiclient": "1.11.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jstree": "3.3.16",
|
"jstree": "3.3.16",
|
||||||
"libarchive.js": "1.3.0",
|
"libarchive.js": "1.3.0",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"markdown-it": "14.0.0",
|
"markdown-it": "14.1.0",
|
||||||
"material-design-icons-iconfont": "6.7.0",
|
"material-design-icons-iconfont": "6.7.0",
|
||||||
"native-promise-only": "0.8.1",
|
"native-promise-only": "0.8.1",
|
||||||
"pdfjs-dist": "3.11.174",
|
"pdfjs-dist": "3.11.174",
|
||||||
|
@ -117,12 +116,12 @@
|
||||||
"react-blurhash": "0.3.0",
|
"react-blurhash": "0.3.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-lazy-load-image-component": "1.6.0",
|
"react-lazy-load-image-component": "1.6.0",
|
||||||
"react-router-dom": "6.21.3",
|
"react-router-dom": "6.22.3",
|
||||||
"resize-observer-polyfill": "1.5.1",
|
"resize-observer-polyfill": "1.5.1",
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.7",
|
||||||
"usehooks-ts": "2.14.0",
|
"usehooks-ts": "2.16.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
|
@ -146,8 +145,8 @@
|
||||||
"start": "npm run serve",
|
"start": "npm run serve",
|
||||||
"serve": "webpack serve --config webpack.dev.js",
|
"serve": "webpack serve --config webpack.dev.js",
|
||||||
"build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js",
|
"build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js",
|
||||||
"build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js",
|
"build:development": "webpack --config webpack.dev.js",
|
||||||
"build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js",
|
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||||
"build:check": "tsc --noEmit",
|
"build:check": "tsc --noEmit",
|
||||||
"escheck": "es-check",
|
"escheck": "es-check",
|
||||||
"lint": "eslint \"./\"",
|
"lint": "eslint \"./\"",
|
||||||
|
|
|
@ -21,7 +21,8 @@ const LIBRARY_PATHS = [
|
||||||
const PLAYBACK_PATHS = [
|
const PLAYBACK_PATHS = [
|
||||||
'/dashboard/playback/transcoding',
|
'/dashboard/playback/transcoding',
|
||||||
'/dashboard/playback/resume',
|
'/dashboard/playback/resume',
|
||||||
'/dashboard/playback/streaming'
|
'/dashboard/playback/streaming',
|
||||||
|
'/dashboard/playback/trickplay'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ServerDrawerSection = () => {
|
const ServerDrawerSection = () => {
|
||||||
|
@ -108,6 +109,9 @@ const ServerDrawerSection = () => {
|
||||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/dashboard/playback/trickplay' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Trickplay')} />
|
||||||
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -9,5 +9,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'users/add', type: AsyncRouteType.Dashboard },
|
{ path: 'users/add', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
|
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/password', type: AsyncRouteType.Dashboard },
|
{ path: 'users/password', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
|
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
|
||||||
|
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard }
|
||||||
];
|
];
|
||||||
|
|
305
src/apps/dashboard/routes/playback/trickplay.tsx
Normal file
305
src/apps/dashboard/routes/playback/trickplay.tsx
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { type FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import globalize from '../../../../scripts/globalize';
|
||||||
|
import Page from '../../../../components/Page';
|
||||||
|
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||||
|
import ButtonElement from '../../../../elements/ButtonElement';
|
||||||
|
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||||
|
import SelectElement from '../../../../elements/SelectElement';
|
||||||
|
import InputElement from '../../../../elements/InputElement';
|
||||||
|
import LinkTrickplayAcceleration from '../../../../components/dashboard/playback/trickplay/LinkTrickplayAcceleration';
|
||||||
|
import loading from '../../../../components/loading/loading';
|
||||||
|
import toast from '../../../../components/toast/toast';
|
||||||
|
import ServerConnections from '../../../../components/ServerConnections';
|
||||||
|
|
||||||
|
function onSaveComplete() {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaybackTrickplay: FunctionComponent = () => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const loadConfig = useCallback((config) => {
|
||||||
|
const page = element.current;
|
||||||
|
const options = config.TrickplayOptions;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options.EnableHwAcceleration;
|
||||||
|
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = options.ScanBehavior;
|
||||||
|
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = options.ProcessPriority;
|
||||||
|
(page.querySelector('#txtInterval') as HTMLInputElement).value = options.Interval;
|
||||||
|
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options.WidthResolutions.join(',');
|
||||||
|
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options.TileWidth;
|
||||||
|
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options.TileHeight;
|
||||||
|
(page.querySelector('#txtQscale') as HTMLInputElement).value = options.Qscale;
|
||||||
|
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options.JpegQuality;
|
||||||
|
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options.ProcessThreads;
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = useCallback(() => {
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
ServerConnections.currentApiClient()?.getServerConfiguration().then(function (config) {
|
||||||
|
loadConfig(config);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to fetch server config', err);
|
||||||
|
});
|
||||||
|
}, [loadConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveConfig = (config: ServerConfiguration) => {
|
||||||
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error('[PlaybackTrickplay] No current apiclient instance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.TrickplayOptions) {
|
||||||
|
throw new Error('Unexpected null TrickplayOptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = config.TrickplayOptions;
|
||||||
|
options.EnableHwAcceleration = (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked;
|
||||||
|
options.ScanBehavior = (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value as TrickplayScanBehavior;
|
||||||
|
options.ProcessPriority = (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value as ProcessPriorityClass;
|
||||||
|
options.Interval = Math.max(1, parseInt((page.querySelector('#txtInterval') as HTMLInputElement).value || '10000', 10));
|
||||||
|
options.WidthResolutions = (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value.replace(' ', '').split(',').map(Number);
|
||||||
|
options.TileWidth = Math.max(1, parseInt((page.querySelector('#txtTileWidth') as HTMLInputElement).value || '10', 10));
|
||||||
|
options.TileHeight = Math.max(1, parseInt((page.querySelector('#txtTileHeight') as HTMLInputElement).value || '10', 10));
|
||||||
|
options.Qscale = Math.min(31, parseInt((page.querySelector('#txtQscale') as HTMLInputElement).value || '4', 10));
|
||||||
|
options.JpegQuality = Math.min(100, parseInt((page.querySelector('#txtJpegQuality') as HTMLInputElement).value || '90', 10));
|
||||||
|
options.ProcessThreads = parseInt((page.querySelector('#txtProcessThreads') as HTMLInputElement).value || '1', 10);
|
||||||
|
|
||||||
|
apiClient.updateServerConfiguration(config).then(() => {
|
||||||
|
onSaveComplete();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to update config', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (e: Event) => {
|
||||||
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error('[PlaybackTrickplay] No current apiclient instance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
apiClient.getServerConfiguration().then(function (config) {
|
||||||
|
saveConfig(config);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to fetch server config', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
(page.querySelector('.trickplayConfigurationForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const optionScanBehavior = () => {
|
||||||
|
let content = '';
|
||||||
|
content += `<option value='NonBlocking'>${globalize.translate('NonBlockingScan')}</option>`;
|
||||||
|
content += `<option value='Blocking'>${globalize.translate('BlockingScan')}</option>`;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionProcessPriority = () => {
|
||||||
|
let content = '';
|
||||||
|
content += `<option value='High'>${globalize.translate('PriorityHigh')}</option>`;
|
||||||
|
content += `<option value='AboveNormal'>${globalize.translate('PriorityAboveNormal')}</option>`;
|
||||||
|
content += `<option value='Normal'>${globalize.translate('PriorityNormal')}</option>`;
|
||||||
|
content += `<option value='BelowNormal'>${globalize.translate('PriorityBelowNormal')}</option>`;
|
||||||
|
content += `<option value='Idle'>${globalize.translate('PriorityIdle')}</option>`;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='trickplayConfigurationPage'
|
||||||
|
className='mainAnimatedPage type-interior playbackConfigurationPage'
|
||||||
|
>
|
||||||
|
<div ref={element} className='content-primary'>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<SectionTitleContainer
|
||||||
|
title={globalize.translate('Trickplay')}
|
||||||
|
isLinkVisible={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className='trickplayConfigurationForm'>
|
||||||
|
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||||
|
<CheckBoxElement
|
||||||
|
className='chkEnableHwAcceleration'
|
||||||
|
title='LabelTrickplayAccel'
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription checkboxFieldDescription'>
|
||||||
|
<LinkTrickplayAcceleration
|
||||||
|
title='LabelTrickplayAccelHelp'
|
||||||
|
href='#/dashboard/playback/transcoding'
|
||||||
|
className='button-link'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='selectContainer fldSelectScanBehavior'>
|
||||||
|
<SelectElement
|
||||||
|
id='selectScanBehavior'
|
||||||
|
label='LabelScanBehavior'
|
||||||
|
>
|
||||||
|
{optionScanBehavior()}
|
||||||
|
</SelectElement>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelScanBehaviorHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='selectContainer fldSelectProcessPriority'>
|
||||||
|
<SelectElement
|
||||||
|
id='selectProcessPriority'
|
||||||
|
label='LabelProcessPriority'
|
||||||
|
>
|
||||||
|
{optionProcessPriority()}
|
||||||
|
</SelectElement>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelProcessPriorityHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtInterval'
|
||||||
|
label='LabelImageInterval'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelImageIntervalHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='text'
|
||||||
|
id='txtWidthResolutions'
|
||||||
|
label='LabelWidthResolutions'
|
||||||
|
options={'required pattern="[0-9,]*"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelWidthResolutionsHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtTileWidth'
|
||||||
|
label='LabelTileWidth'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTileWidthHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtTileHeight'
|
||||||
|
label='LabelTileHeight'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTileHeightHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtJpegQuality'
|
||||||
|
label='LabelJpegQuality'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1" max="100"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelJpegQualityHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtQscale'
|
||||||
|
label='LabelQscale'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="2" max="31"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelQscaleHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtProcessThreads'
|
||||||
|
label='LabelTrickplayThreads'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="0"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTrickplayThreadsHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ButtonElement
|
||||||
|
type='submit'
|
||||||
|
className='raised button-submit block'
|
||||||
|
title='Save'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlaybackTrickplay;
|
|
@ -49,6 +49,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
const showUserMenu = (elem: HTMLElement) => {
|
const showUserMenu = (elem: HTMLElement) => {
|
||||||
const card = dom.parentWithClass(elem, 'card');
|
const card = dom.parentWithClass(elem, 'card');
|
||||||
const userId = card?.getAttribute('data-userid');
|
const userId = card?.getAttribute('data-userid');
|
||||||
|
const username = card?.getAttribute('data-username');
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error('Unexpected null user id');
|
console.error('Unexpected null user id');
|
||||||
|
@ -106,7 +107,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteUser(userId);
|
deleteUser(userId, username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -117,12 +118,13 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = (id: string) => {
|
const deleteUser = (id: string, username?: string | null) => {
|
||||||
const msg = globalize.translate('DeleteUserConfirmation');
|
const title = username ? globalize.translate('DeleteName', username) : globalize.translate('DeleteUser');
|
||||||
|
const text = globalize.translate('DeleteUserConfirmation');
|
||||||
|
|
||||||
confirm({
|
confirm({
|
||||||
title: globalize.translate('DeleteUser'),
|
title,
|
||||||
text: msg,
|
text,
|
||||||
confirmText: globalize.translate('Delete'),
|
confirmText: globalize.translate('Delete'),
|
||||||
primary: 'delete'
|
primary: 'delete'
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import escapeHTML from 'escape-html';
|
||||||
import globalize from '../../../../scripts/globalize';
|
import globalize from '../../../../scripts/globalize';
|
||||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||||
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
||||||
import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList';
|
import TagList from '../../../../components/dashboard/users/TagList';
|
||||||
import ButtonElement from '../../../../elements/ButtonElement';
|
import ButtonElement from '../../../../elements/ButtonElement';
|
||||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||||
|
@ -16,6 +16,8 @@ import { getParameterByName } from '../../../../utils/url';
|
||||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||||
import SelectElement from '../../../../elements/SelectElement';
|
import SelectElement from '../../../../elements/SelectElement';
|
||||||
import Page from '../../../../components/Page';
|
import Page from '../../../../components/Page';
|
||||||
|
import prompt from '../../../../components/prompt/prompt';
|
||||||
|
import ServerConnections from 'components/ServerConnections';
|
||||||
|
|
||||||
type UnratedItem = {
|
type UnratedItem = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -23,12 +25,44 @@ type UnratedItem = {
|
||||||
checkedAttribute: string
|
checkedAttribute: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleSaveUser(
|
||||||
|
page: HTMLDivElement,
|
||||||
|
getSchedulesFromPage: () => AccessSchedule[],
|
||||||
|
getAllowedTagsFromPage: () => string[],
|
||||||
|
getBlockedTagsFromPage: () => string[],
|
||||||
|
onSaveComplete: () => void
|
||||||
|
) {
|
||||||
|
return (user: UserDto) => {
|
||||||
|
const userId = user.Id;
|
||||||
|
const userPolicy = user.Policy;
|
||||||
|
if (!userId || !userPolicy) {
|
||||||
|
throw new Error('Unexpected null user id or policy');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
|
||||||
|
userPolicy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
|
||||||
|
userPolicy.BlockUnratedItems = Array.prototype.filter
|
||||||
|
.call(page.querySelectorAll('.chkUnratedItem'), i => i.checked)
|
||||||
|
.map(i => i.getAttribute('data-itemtype'));
|
||||||
|
userPolicy.AccessSchedules = getSchedulesFromPage();
|
||||||
|
userPolicy.AllowedTags = getAllowedTagsFromPage();
|
||||||
|
userPolicy.BlockedTags = getBlockedTagsFromPage();
|
||||||
|
ServerConnections.getCurrentApiClientAsync()
|
||||||
|
.then(apiClient => apiClient.updateUserPolicy(userId, userPolicy))
|
||||||
|
.then(() => onSaveComplete())
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[userparentalcontrol] failed to update user policy', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const UserParentalControl: FunctionComponent = () => {
|
const UserParentalControl: FunctionComponent = () => {
|
||||||
const [ userName, setUserName ] = useState('');
|
const [ userName, setUserName ] = useState('');
|
||||||
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
|
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
|
||||||
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
|
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
|
||||||
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
|
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
|
||||||
const [ blockedTags, setBlockedTags ] = useState([]);
|
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
|
||||||
|
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
|
||||||
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -106,7 +140,28 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadBlockedTags = useCallback((tags) => {
|
const loadAllowedTags = useCallback((tags: string[]) => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAllowedTags(tags);
|
||||||
|
|
||||||
|
const allowedTagsElem = page.querySelector('.allowedTags') as HTMLDivElement;
|
||||||
|
|
||||||
|
for (const btnDeleteTag of allowedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
||||||
|
btnDeleteTag.addEventListener('click', function () {
|
||||||
|
const tag = btnDeleteTag.getAttribute('data-tag');
|
||||||
|
const newTags = tags.filter(t => t !== tag);
|
||||||
|
loadAllowedTags(newTags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadBlockedTags = useCallback((tags: string[]) => {
|
||||||
const page = element.current;
|
const page = element.current;
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
@ -121,9 +176,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
||||||
btnDeleteTag.addEventListener('click', function () {
|
btnDeleteTag.addEventListener('click', function () {
|
||||||
const tag = btnDeleteTag.getAttribute('data-tag');
|
const tag = btnDeleteTag.getAttribute('data-tag');
|
||||||
const newTags = tags.filter(function (t: string) {
|
const newTags = tags.filter(t => t !== tag);
|
||||||
return t != tag;
|
|
||||||
});
|
|
||||||
loadBlockedTags(newTags);
|
loadBlockedTags(newTags);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -145,15 +198,13 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
btnDelete.addEventListener('click', function () {
|
btnDelete.addEventListener('click', function () {
|
||||||
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
|
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
|
||||||
schedules.splice(index, 1);
|
schedules.splice(index, 1);
|
||||||
const newindex = schedules.filter(function (i: number) {
|
const newindex = schedules.filter((i: number) => i != index);
|
||||||
return i != index;
|
|
||||||
});
|
|
||||||
renderAccessSchedule(newindex);
|
renderAccessSchedule(newindex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadUser = useCallback((user, allParentalRatings) => {
|
const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => {
|
||||||
const page = element.current;
|
const page = element.current;
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
@ -161,34 +212,33 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserName(user.Name);
|
setUserName(user.Name || '');
|
||||||
LibraryMenu.setTitle(user.Name);
|
LibraryMenu.setTitle(user.Name);
|
||||||
loadUnratedItems(user);
|
loadUnratedItems(user);
|
||||||
|
|
||||||
loadBlockedTags(user.Policy.BlockedTags);
|
loadAllowedTags(user.Policy?.AllowedTags || []);
|
||||||
|
loadBlockedTags(user.Policy?.BlockedTags || []);
|
||||||
populateRatings(allParentalRatings);
|
populateRatings(allParentalRatings);
|
||||||
|
|
||||||
let ratingValue = '';
|
let ratingValue = '';
|
||||||
|
if (user.Policy?.MaxParentalRating) {
|
||||||
if (user.Policy.MaxParentalRating != null) {
|
allParentalRatings.forEach(rating => {
|
||||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
if (rating.Value && user.Policy?.MaxParentalRating && user.Policy.MaxParentalRating >= rating.Value) {
|
||||||
const rating = allParentalRatings[i];
|
ratingValue = `${rating.Value}`;
|
||||||
|
|
||||||
if (user.Policy.MaxParentalRating >= rating.Value) {
|
|
||||||
ratingValue = rating.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
|
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
|
||||||
|
|
||||||
if (user.Policy.IsAdministrator) {
|
if (user.Policy?.IsAdministrator) {
|
||||||
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
|
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
|
||||||
} else {
|
} else {
|
||||||
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
|
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
|
||||||
}
|
}
|
||||||
renderAccessSchedule(user.Policy.AccessSchedules || []);
|
renderAccessSchedule(user.Policy?.AccessSchedules || []);
|
||||||
loading.hide();
|
loading.hide();
|
||||||
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
}, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
||||||
|
|
||||||
const loadData = useCallback(() => {
|
const loadData = useCallback(() => {
|
||||||
loading.show();
|
loading.show();
|
||||||
|
@ -212,32 +262,6 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
const onSaveComplete = () => {
|
|
||||||
loading.hide();
|
|
||||||
toast(globalize.translate('SettingsSaved'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveUser = (user: UserDto) => {
|
|
||||||
if (!user.Id || !user.Policy) {
|
|
||||||
throw new Error('Unexpected null user id or policy');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
|
|
||||||
user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
|
|
||||||
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
|
|
||||||
return i.checked;
|
|
||||||
}).map(function (i) {
|
|
||||||
return i.getAttribute('data-itemtype');
|
|
||||||
});
|
|
||||||
user.Policy.AccessSchedules = getSchedulesFromPage();
|
|
||||||
user.Policy.BlockedTags = getBlockedTagsFromPage();
|
|
||||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
|
||||||
onSaveComplete();
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[userparentalcontrol] failed to update user policy', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
|
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
|
||||||
schedule = schedule || {};
|
schedule = schedule || {};
|
||||||
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
||||||
|
@ -270,6 +294,27 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
}) as AccessSchedule[];
|
}) as AccessSchedule[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllowedTagsFromPage = () => {
|
||||||
|
return Array.prototype.map.call(page.querySelectorAll('.allowedTag'), function (elem) {
|
||||||
|
return elem.getAttribute('data-tag');
|
||||||
|
}) as string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAllowedTagPopup = () => {
|
||||||
|
prompt({
|
||||||
|
label: globalize.translate('LabelTag')
|
||||||
|
}).then(function (value) {
|
||||||
|
const tags = getAllowedTagsFromPage();
|
||||||
|
|
||||||
|
if (tags.indexOf(value) == -1) {
|
||||||
|
tags.push(value);
|
||||||
|
loadAllowedTags(tags);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// prompt closed
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getBlockedTagsFromPage = () => {
|
const getBlockedTagsFromPage = () => {
|
||||||
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
|
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
|
||||||
return elem.getAttribute('data-tag');
|
return elem.getAttribute('data-tag');
|
||||||
|
@ -277,7 +322,6 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const showBlockedTagPopup = () => {
|
const showBlockedTagPopup = () => {
|
||||||
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
|
|
||||||
prompt({
|
prompt({
|
||||||
label: globalize.translate('LabelTag')
|
label: globalize.translate('LabelTag')
|
||||||
}).then(function (value) {
|
}).then(function (value) {
|
||||||
|
@ -290,11 +334,15 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// prompt closed
|
// prompt closed
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
|
||||||
console.error('[userparentalcontrol] failed to load prompt', err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSaveComplete = () => {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
|
||||||
|
|
||||||
const onSubmit = (e: Event) => {
|
const onSubmit = (e: Event) => {
|
||||||
loading.show();
|
loading.show();
|
||||||
const userId = getParameterByName('userId');
|
const userId = getParameterByName('userId');
|
||||||
|
@ -318,12 +366,16 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
}, -1);
|
}, -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', function () {
|
||||||
|
showAllowedTagPopup();
|
||||||
|
});
|
||||||
|
|
||||||
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
|
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
|
||||||
showBlockedTagPopup();
|
showBlockedTagPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||||
}, [loadBlockedTags, loadData, renderAccessSchedule]);
|
}, [loadAllowedTags, loadBlockedTags, loadData, renderAccessSchedule]);
|
||||||
|
|
||||||
const optionMaxParentalRating = () => {
|
const optionMaxParentalRating = () => {
|
||||||
let content = '';
|
let content = '';
|
||||||
|
@ -378,6 +430,30 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
<div className='verticalSection' style={{ marginBottom: '2em' }}>
|
||||||
|
<SectionTitleContainer
|
||||||
|
SectionClassName='detailSectionHeader'
|
||||||
|
title={globalize.translate('LabelAllowContentWithTags')}
|
||||||
|
isBtnVisible={true}
|
||||||
|
btnId='btnAddAllowedTag'
|
||||||
|
btnClassName='fab submit sectionTitleButton'
|
||||||
|
btnTitle='Add'
|
||||||
|
btnIcon='add'
|
||||||
|
isLinkVisible={false}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('AllowContentWithTagsHelp')}
|
||||||
|
</div>
|
||||||
|
<div className='allowedTags' style={{ marginTop: '.5em' }}>
|
||||||
|
{allowedTags?.map(tag => {
|
||||||
|
return <TagList
|
||||||
|
key={tag}
|
||||||
|
tag={tag}
|
||||||
|
tagType='allowedTag'
|
||||||
|
/>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className='verticalSection' style={{ marginBottom: '2em' }}>
|
<div className='verticalSection' style={{ marginBottom: '2em' }}>
|
||||||
<SectionTitleContainer
|
<SectionTitleContainer
|
||||||
SectionClassName='detailSectionHeader'
|
SectionClassName='detailSectionHeader'
|
||||||
|
@ -389,11 +465,15 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
btnIcon='add'
|
btnIcon='add'
|
||||||
isLinkVisible={false}
|
isLinkVisible={false}
|
||||||
/>
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('BlockContentWithTagsHelp')}
|
||||||
|
</div>
|
||||||
<div className='blockedTags' style={{ marginTop: '.5em' }}>
|
<div className='blockedTags' style={{ marginTop: '.5em' }}>
|
||||||
{blockedTags.map(tag => {
|
{blockedTags.map(tag => {
|
||||||
return <BlockedTagList
|
return <TagList
|
||||||
key={tag}
|
key={tag}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
|
tagType='blockedTag'
|
||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -182,6 +182,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
||||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
||||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
||||||
|
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = user.Policy.EnableSubtitleManagement;
|
||||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
||||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
||||||
|
@ -240,6 +241,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
||||||
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
||||||
|
user.Policy.EnableSubtitleManagement = (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked;
|
||||||
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
||||||
|
@ -392,6 +394,11 @@ const UserEdit: FunctionComponent = () => {
|
||||||
className='chkEnableCollectionManagement'
|
className='chkEnableCollectionManagement'
|
||||||
title='AllowCollectionManagement'
|
title='AllowCollectionManagement'
|
||||||
/>
|
/>
|
||||||
|
<CheckBoxElement
|
||||||
|
labelClassName='checkboxContainer'
|
||||||
|
className='chkEnableSubtitleManagement'
|
||||||
|
title='AllowSubtitleManagement'
|
||||||
|
/>
|
||||||
<div id='featureAccessFields' className='verticalSection'>
|
<div id='featureAccessFields' className='verticalSection'>
|
||||||
<h2 className='paperListLabel'>
|
<h2 className='paperListLabel'>
|
||||||
{globalize.translate('HeaderFeatureAccess')}
|
{globalize.translate('HeaderFeatureAccess')}
|
||||||
|
|
|
@ -8,5 +8,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
|
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
|
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental }
|
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental },
|
||||||
|
{ path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental }
|
||||||
];
|
];
|
||||||
|
|
|
@ -25,12 +25,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'user/controls/index',
|
controller: 'user/controls/index',
|
||||||
view: 'user/controls/index.html'
|
view: 'user/controls/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'mypreferencesdisplay.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'user/display/index',
|
|
||||||
view: 'user/display/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'mypreferenceshome.html',
|
path: 'mypreferenceshome.html',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
203
src/apps/experimental/routes/user/display/DisplayPreferences.tsx
Normal file
203
src/apps/experimental/routes/user/display/DisplayPreferences.tsx
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
import { appHost } from 'components/apphost';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
import { useScreensavers } from './hooks/useScreensavers';
|
||||||
|
import { useServerThemes } from './hooks/useServerThemes';
|
||||||
|
|
||||||
|
interface DisplayPreferencesProps {
|
||||||
|
onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void;
|
||||||
|
values: DisplaySettingsValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DisplayPreferences({ onChange, values }: Readonly<DisplayPreferencesProps>) {
|
||||||
|
const { user } = useApi();
|
||||||
|
const { screensavers } = useScreensavers();
|
||||||
|
const { themes } = useServerThemes();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography variant='h2'>{globalize.translate('Display')}</Typography>
|
||||||
|
|
||||||
|
{ appHost.supports('displaymode') && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-layout-label'>{globalize.translate('LabelDisplayMode')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
aria-describedby='display-settings-layout-description'
|
||||||
|
inputProps={{
|
||||||
|
name: 'layout'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-layout-label'
|
||||||
|
onChange={onChange}
|
||||||
|
value={values.layout}
|
||||||
|
>
|
||||||
|
<MenuItem value='auto'>{globalize.translate('Auto')}</MenuItem>
|
||||||
|
<MenuItem value='desktop'>{globalize.translate('Desktop')}</MenuItem>
|
||||||
|
<MenuItem value='mobile'>{globalize.translate('Mobile')}</MenuItem>
|
||||||
|
<MenuItem value='tv'>{globalize.translate('TV')}</MenuItem>
|
||||||
|
<MenuItem value='experimental'>{globalize.translate('Experimental')}</MenuItem>
|
||||||
|
</Select>
|
||||||
|
<FormHelperText component={Stack} id='display-settings-layout-description'>
|
||||||
|
<span>{globalize.translate('DisplayModeHelp')}</span>
|
||||||
|
<span>{globalize.translate('LabelPleaseRestart')}</span>
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ themes.length > 0 && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-theme-label'>{globalize.translate('LabelTheme')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
inputProps={{
|
||||||
|
name: 'theme'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-theme-label'
|
||||||
|
onChange={onChange}
|
||||||
|
value={values.theme}
|
||||||
|
>
|
||||||
|
{ ...themes.map(({ id, name }) => (
|
||||||
|
<MenuItem key={id} value={id}>{name}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) }
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-disable-css-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.disableCustomCss}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('DisableCustomCss')}
|
||||||
|
name='disableCustomCss'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-disable-css-description'>
|
||||||
|
{globalize.translate('LabelDisableCustomCss')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
aria-describedby='display-settings-custom-css-description'
|
||||||
|
value={values.customCss}
|
||||||
|
label={globalize.translate('LabelCustomCss')}
|
||||||
|
multiline
|
||||||
|
name='customCss'
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-custom-css-description'>
|
||||||
|
{globalize.translate('LabelLocalCustomCss')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{ themes.length > 0 && user?.Policy?.IsAdministrator && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-dashboard-theme-label'>{globalize.translate('LabelDashboardTheme')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
inputProps={{
|
||||||
|
name: 'dashboardTheme'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-dashboard-theme-label'
|
||||||
|
onChange={ onChange }
|
||||||
|
value={ values.dashboardTheme }
|
||||||
|
>
|
||||||
|
{ ...themes.map(({ id, name }) => (
|
||||||
|
<MenuItem key={ id } value={ id }>{ name }</MenuItem>
|
||||||
|
)) }
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ screensavers.length > 0 && appHost.supports('screensaver') && (
|
||||||
|
<Fragment>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-screensaver-label'>{globalize.translate('LabelScreensaver')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
inputProps={{
|
||||||
|
name: 'screensaver'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-screensaver-label'
|
||||||
|
onChange={onChange}
|
||||||
|
value={values.screensaver}
|
||||||
|
>
|
||||||
|
{ ...screensavers.map(({ id, name }) => (
|
||||||
|
<MenuItem key={id} value={id}>{name}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
aria-describedby='display-settings-screensaver-interval-description'
|
||||||
|
value={values.screensaverInterval}
|
||||||
|
inputProps={{
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '3600',
|
||||||
|
min: '1',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1',
|
||||||
|
type: 'number'
|
||||||
|
}}
|
||||||
|
label={globalize.translate('LabelBackdropScreensaverInterval')}
|
||||||
|
name='screensaverInterval'
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-screensaver-interval-description'>
|
||||||
|
{globalize.translate('LabelBackdropScreensaverIntervalHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Fragment>
|
||||||
|
) }
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-faster-animations-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableFasterAnimation}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('EnableFasterAnimations')}
|
||||||
|
name='enableFasterAnimation'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-faster-animations-description'>
|
||||||
|
{globalize.translate('EnableFasterAnimationsHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-blurhash-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableBlurHash}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('EnableBlurHash')}
|
||||||
|
name='enableBlurHash'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-blurhash-description'>
|
||||||
|
{globalize.translate('EnableBlurHashHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
|
||||||
|
interface ItemDetailPreferencesProps {
|
||||||
|
onChange: (event: React.SyntheticEvent) => void;
|
||||||
|
values: DisplaySettingsValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemDetailPreferences({ onChange, values }: Readonly<ItemDetailPreferencesProps>) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography variant='h2'>{globalize.translate('ItemDetails')}</Typography>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-item-details-banner-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableItemDetailsBanner}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('EnableDetailsBanner')}
|
||||||
|
name='enableItemDetailsBanner'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-item-details-banner-description'>
|
||||||
|
{globalize.translate('EnableDetailsBannerHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
114
src/apps/experimental/routes/user/display/LibraryPreferences.tsx
Normal file
114
src/apps/experimental/routes/user/display/LibraryPreferences.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
|
||||||
|
interface LibraryPreferencesProps {
|
||||||
|
onChange: (event: React.SyntheticEvent) => void;
|
||||||
|
values: DisplaySettingsValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LibraryPreferences({ onChange, values }: Readonly<LibraryPreferencesProps>) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography variant='h2'>{globalize.translate('HeaderLibraries')}</Typography>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
aria-describedby='display-settings-lib-pagesize-description'
|
||||||
|
inputProps={{
|
||||||
|
type: 'number',
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '1000',
|
||||||
|
min: '0',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1'
|
||||||
|
}}
|
||||||
|
value={values.libraryPageSize}
|
||||||
|
label={globalize.translate('LabelLibraryPageSize')}
|
||||||
|
name='libraryPageSize'
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-lib-pagesize-description'>
|
||||||
|
{globalize.translate('LabelLibraryPageSizeHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-lib-backdrops-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableLibraryBackdrops}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('Backdrops')}
|
||||||
|
name='enableLibraryBackdrops'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-lib-backdrops-description'>
|
||||||
|
{globalize.translate('EnableBackdropsHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-lib-theme-songs-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableLibraryThemeSongs}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ThemeSongs')}
|
||||||
|
name='enableLibraryThemeSongs'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-lib-theme-songs-description'>
|
||||||
|
{globalize.translate('EnableThemeSongsHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-lib-theme-videos-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableLibraryThemeVideos}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ThemeVideos')}
|
||||||
|
name='enableLibraryThemeVideos'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-lib-theme-videos-description'>
|
||||||
|
{globalize.translate('EnableThemeVideosHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-show-missing-episodes-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.displayMissingEpisodes}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('DisplayMissingEpisodesWithinSeasons')}
|
||||||
|
name='displayMissingEpisodes'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-show-missing-episodes-description'>
|
||||||
|
{globalize.translate('DisplayMissingEpisodesWithinSeasonsHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { appHost } from 'components/apphost';
|
||||||
|
import datetime from 'scripts/datetime';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
|
||||||
|
interface LocalizationPreferencesProps {
|
||||||
|
onChange: (event: SelectChangeEvent) => void;
|
||||||
|
values: DisplaySettingsValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalizationPreferences({ onChange, values }: Readonly<LocalizationPreferencesProps>) {
|
||||||
|
if (!appHost.supports('displaylanguage') && !datetime.supportsLocalization()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography variant='h2'>{globalize.translate('Localization')}</Typography>
|
||||||
|
|
||||||
|
{ appHost.supports('displaylanguage') && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-language-label'>{globalize.translate('LabelDisplayLanguage')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
aria-describedby='display-settings-language-description'
|
||||||
|
inputProps={{
|
||||||
|
name: 'language'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-language-label'
|
||||||
|
onChange={onChange}
|
||||||
|
value={values.language}
|
||||||
|
>
|
||||||
|
{ ...LANGUAGE_OPTIONS.map(({ value, label }) => (
|
||||||
|
<MenuItem key={value } value={value}>{ label }</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<FormHelperText component={Stack} id='display-settings-language-description'>
|
||||||
|
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
|
||||||
|
{ appHost.supports('externallinks') && (
|
||||||
|
<Link
|
||||||
|
href='https://github.com/jellyfin/jellyfin'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{globalize.translate('LearnHowYouCanContribute')}
|
||||||
|
</Link>
|
||||||
|
) }
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ datetime.supportsLocalization() && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='display-settings-locale-label'>{globalize.translate('LabelDateTimeLocale')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
inputProps={{
|
||||||
|
name: 'dateTimeLocale'
|
||||||
|
}}
|
||||||
|
labelId='display-settings-locale-label'
|
||||||
|
onChange={onChange}
|
||||||
|
value={values.dateTimeLocale}
|
||||||
|
>
|
||||||
|
{...DATE_LOCALE_OPTIONS.map(({ value, label }) => (
|
||||||
|
<MenuItem key={value} value={value}>{label}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) }
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
|
||||||
|
interface NextUpPreferencesProps {
|
||||||
|
onChange: (event: React.SyntheticEvent) => void;
|
||||||
|
values: DisplaySettingsValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NextUpPreferences({ onChange, values }: Readonly<NextUpPreferencesProps>) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography variant='h2'>{globalize.translate('NextUp')}</Typography>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
aria-describedby='display-settings-max-days-next-up-description'
|
||||||
|
value={values.maxDaysForNextUp}
|
||||||
|
inputProps={{
|
||||||
|
type: 'number',
|
||||||
|
inputMode: 'numeric',
|
||||||
|
max: '1000',
|
||||||
|
min: '0',
|
||||||
|
pattern: '[0-9]',
|
||||||
|
required: true,
|
||||||
|
step: '1'
|
||||||
|
}}
|
||||||
|
label={globalize.translate('LabelMaxDaysForNextUp')}
|
||||||
|
name='maxDaysForNextUp'
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-max-days-next-up-description'>
|
||||||
|
{globalize.translate('LabelMaxDaysForNextUpHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-next-up-rewatching-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.enableRewatchingInNextUp}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('EnableRewatchingNextUp')}
|
||||||
|
name='enableRewatchingInNextUp'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-next-up-rewatching-description'>
|
||||||
|
{globalize.translate('EnableRewatchingNextUpHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
aria-describedby='display-settings-next-up-images-description'
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.episodeImagesInNextUp}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('UseEpisodeImagesInNextUp')}
|
||||||
|
name='episodeImagesInNextUp'
|
||||||
|
/>
|
||||||
|
<FormHelperText id='display-settings-next-up-images-description'>
|
||||||
|
{globalize.translate('UseEpisodeImagesInNextUpHelp')}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
79
src/apps/experimental/routes/user/display/constants.ts
Normal file
79
src/apps/experimental/routes/user/display/constants.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
export const LANGUAGE_OPTIONS = [
|
||||||
|
{ value: 'auto', label: globalize.translate('Auto') },
|
||||||
|
{ value: 'af', label: 'Afrikaans' },
|
||||||
|
{ value: 'ar', label: 'العربية' },
|
||||||
|
{ value: 'be-BY', label: 'Беларуская' },
|
||||||
|
{ value: 'bg-BG', label: 'Български' },
|
||||||
|
{ value: 'bn_BD', label: 'বাংলা (বাংলাদেশ)' },
|
||||||
|
{ value: 'ca', label: 'Català' },
|
||||||
|
{ value: 'cs', label: 'Čeština' },
|
||||||
|
{ value: 'cy', label: 'Cymraeg' },
|
||||||
|
{ value: 'da', label: 'Dansk' },
|
||||||
|
{ value: 'de', label: 'Deutsch' },
|
||||||
|
{ value: 'el', label: 'Ελληνικά' },
|
||||||
|
{ value: 'en-GB', label: 'English (United Kingdom)' },
|
||||||
|
{ value: 'en-US', label: 'English' },
|
||||||
|
{ value: 'eo', label: 'Esperanto' },
|
||||||
|
{ value: 'es', label: 'Español' },
|
||||||
|
{ value: 'es_419', label: 'Español americano' },
|
||||||
|
{ value: 'es-AR', label: 'Español (Argentina)' },
|
||||||
|
{ value: 'es_DO', label: 'Español (Dominicana)' },
|
||||||
|
{ value: 'es-MX', label: 'Español (México)' },
|
||||||
|
{ value: 'et', label: 'Eesti' },
|
||||||
|
{ value: 'eu', label: 'Euskara' },
|
||||||
|
{ value: 'fa', label: 'فارسی' },
|
||||||
|
{ value: 'fi', label: 'Suomi' },
|
||||||
|
{ value: 'fil', label: 'Filipino' },
|
||||||
|
{ value: 'fr', label: 'Français' },
|
||||||
|
{ value: 'fr-CA', label: 'Français (Canada)' },
|
||||||
|
{ value: 'gl', label: 'Galego' },
|
||||||
|
{ value: 'gsw', label: 'Schwiizerdütsch' },
|
||||||
|
{ value: 'he', label: 'עִבְרִית' },
|
||||||
|
{ value: 'hi-IN', label: 'हिन्दी' },
|
||||||
|
{ value: 'hr', label: 'Hrvatski' },
|
||||||
|
{ value: 'hu', label: 'Magyar' },
|
||||||
|
{ value: 'id', label: 'Bahasa Indonesia' },
|
||||||
|
{ value: 'is-IS', label: 'Íslenska' },
|
||||||
|
{ value: 'it', label: 'Italiano' },
|
||||||
|
{ value: 'ja', label: '日本語' },
|
||||||
|
{ value: 'kk', label: 'Qazaqşa' },
|
||||||
|
{ value: 'ko', label: '한국어' },
|
||||||
|
{ value: 'lt-LT', label: 'Lietuvių' },
|
||||||
|
{ value: 'lv', label: 'Latviešu' },
|
||||||
|
{ value: 'mk', label: 'Македонски' },
|
||||||
|
{ value: 'ml', label: 'മലയാളം' },
|
||||||
|
{ value: 'mr', label: 'मराठी' },
|
||||||
|
{ value: 'ms', label: 'Bahasa Melayu' },
|
||||||
|
{ value: 'nb', label: 'Norsk bokmål' },
|
||||||
|
{ value: 'ne', label: 'नेपाली' },
|
||||||
|
{ value: 'nl', label: 'Nederlands' },
|
||||||
|
{ value: 'nn', label: 'Norsk nynorsk' },
|
||||||
|
{ value: 'pa', label: 'ਪੰਜਾਬੀ' },
|
||||||
|
{ value: 'pl', label: 'Polski' },
|
||||||
|
{ value: 'pr', label: 'Pirate' },
|
||||||
|
{ value: 'pt', label: 'Português' },
|
||||||
|
{ value: 'pt-BR', label: 'Português (Brasil)' },
|
||||||
|
{ value: 'pt-PT', label: 'Português (Portugal)' },
|
||||||
|
{ value: 'ro', label: 'Românește' },
|
||||||
|
{ value: 'ru', label: 'Русский' },
|
||||||
|
{ value: 'sk', label: 'Slovenčina' },
|
||||||
|
{ value: 'sl-SI', label: 'Slovenščina' },
|
||||||
|
{ value: 'sq', label: 'Shqip' },
|
||||||
|
{ value: 'sr', label: 'Српски' },
|
||||||
|
{ value: 'sv', label: 'Svenska' },
|
||||||
|
{ value: 'ta', label: 'தமிழ்' },
|
||||||
|
{ value: 'te', label: 'తెలుగు' },
|
||||||
|
{ value: 'th', label: 'ภาษาไทย' },
|
||||||
|
{ value: 'tr', label: 'Türkçe' },
|
||||||
|
{ value: 'uk', label: 'Українська' },
|
||||||
|
{ value: 'ur_PK', label: ' اُردُو' },
|
||||||
|
{ value: 'vi', label: 'Tiếng Việt' },
|
||||||
|
{ value: 'zh-CN', label: '汉语 (简化字)' },
|
||||||
|
{ value: 'zh-TW', label: '漢語 (繁体字)' },
|
||||||
|
{ value: 'zh-HK', label: '廣東話 (香港)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// NOTE: Option `Euskara` (eu) does not exist in legacy date locale options.
|
||||||
|
export const DATE_LOCALE_OPTIONS = LANGUAGE_OPTIONS.filter(({ value }) => value !== 'eu');
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import toast from 'components/toast/toast';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { DisplaySettingsValues } from '../types';
|
||||||
|
import { useDisplaySettings } from './useDisplaySettings';
|
||||||
|
|
||||||
|
export function useDisplaySettingForm() {
|
||||||
|
const [urlParams] = useSearchParams();
|
||||||
|
const {
|
||||||
|
displaySettings,
|
||||||
|
loading,
|
||||||
|
saveDisplaySettings
|
||||||
|
} = useDisplaySettings({ userId: urlParams.get('userId') });
|
||||||
|
const [formValues, setFormValues] = useState<DisplaySettingsValues>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && displaySettings && !formValues) {
|
||||||
|
setFormValues(displaySettings);
|
||||||
|
}
|
||||||
|
}, [formValues, loading, displaySettings]);
|
||||||
|
|
||||||
|
const updateField = useCallback(({ name, value }) => {
|
||||||
|
if (formValues) {
|
||||||
|
setFormValues({
|
||||||
|
...formValues,
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [formValues, setFormValues]);
|
||||||
|
|
||||||
|
const submitChanges = useCallback(async () => {
|
||||||
|
if (formValues) {
|
||||||
|
await saveDisplaySettings(formValues);
|
||||||
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
}
|
||||||
|
}, [formValues, saveDisplaySettings]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
values: formValues,
|
||||||
|
submitChanges,
|
||||||
|
updateField
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { ApiClient } from 'jellyfin-apiclient';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { appHost } from 'components/apphost';
|
||||||
|
import layoutManager from 'components/layoutManager';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import themeManager from 'scripts/themeManager';
|
||||||
|
import { currentSettings, UserSettings } from 'scripts/settings/userSettings';
|
||||||
|
import { DisplaySettingsValues } from '../types';
|
||||||
|
|
||||||
|
interface UseDisplaySettingsParams {
|
||||||
|
userId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDisplaySettings({ userId }: UseDisplaySettingsParams) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [userSettings, setUserSettings] = useState<UserSettings>();
|
||||||
|
const [displaySettings, setDisplaySettings] = useState<DisplaySettingsValues>();
|
||||||
|
const { __legacyApiClient__, user: currentUser } = useApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userId || !currentUser || !__legacyApiClient__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const loadedSettings = await loadDisplaySettings({ api: __legacyApiClient__, currentUser, userId });
|
||||||
|
|
||||||
|
setDisplaySettings(loadedSettings.displaySettings);
|
||||||
|
setUserSettings(loadedSettings.userSettings);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
}, [__legacyApiClient__, currentUser, userId]);
|
||||||
|
|
||||||
|
const saveSettings = useCallback(async (newSettings: DisplaySettingsValues) => {
|
||||||
|
if (!userId || !userSettings || !__legacyApiClient__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return saveDisplaySettings({
|
||||||
|
api: __legacyApiClient__,
|
||||||
|
newDisplaySettings: newSettings,
|
||||||
|
userSettings,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
}, [__legacyApiClient__, userSettings, userId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
displaySettings,
|
||||||
|
loading,
|
||||||
|
saveDisplaySettings: saveSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadDisplaySettingsParams {
|
||||||
|
currentUser: UserDto;
|
||||||
|
userId?: string;
|
||||||
|
api: ApiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDisplaySettings({
|
||||||
|
currentUser,
|
||||||
|
userId,
|
||||||
|
api
|
||||||
|
}: LoadDisplaySettingsParams) {
|
||||||
|
const settings = (!userId || userId === currentUser?.Id) ? currentSettings : new UserSettings();
|
||||||
|
const user = (!userId || userId === currentUser?.Id) ? currentUser : await api.getUser(userId);
|
||||||
|
|
||||||
|
await settings.setUserInfo(userId, api);
|
||||||
|
|
||||||
|
const displaySettings = {
|
||||||
|
customCss: settings.customCss(),
|
||||||
|
dashboardTheme: settings.dashboardTheme() || 'auto',
|
||||||
|
dateTimeLocale: settings.dateTimeLocale() || 'auto',
|
||||||
|
disableCustomCss: Boolean(settings.disableCustomCss()),
|
||||||
|
displayMissingEpisodes: user?.Configuration?.DisplayMissingEpisodes ?? false,
|
||||||
|
enableBlurHash: Boolean(settings.enableBlurhash()),
|
||||||
|
enableFasterAnimation: Boolean(settings.enableFastFadein()),
|
||||||
|
enableItemDetailsBanner: Boolean(settings.detailsBanner()),
|
||||||
|
enableLibraryBackdrops: Boolean(settings.enableBackdrops()),
|
||||||
|
enableLibraryThemeSongs: Boolean(settings.enableThemeSongs()),
|
||||||
|
enableLibraryThemeVideos: Boolean(settings.enableThemeVideos()),
|
||||||
|
enableRewatchingInNextUp: Boolean(settings.enableRewatchingInNextUp()),
|
||||||
|
episodeImagesInNextUp: Boolean(settings.useEpisodeImagesInNextUpAndResume()),
|
||||||
|
language: settings.language() || 'auto',
|
||||||
|
layout: layoutManager.getSavedLayout() || 'auto',
|
||||||
|
libraryPageSize: settings.libraryPageSize(),
|
||||||
|
maxDaysForNextUp: settings.maxDaysForNextUp(),
|
||||||
|
screensaver: settings.screensaver() || 'none',
|
||||||
|
screensaverInterval: settings.backdropScreensaverInterval(),
|
||||||
|
theme: settings.theme()
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
displaySettings,
|
||||||
|
userSettings: settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SaveDisplaySettingsParams {
|
||||||
|
api: ApiClient;
|
||||||
|
newDisplaySettings: DisplaySettingsValues
|
||||||
|
userSettings: UserSettings;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDisplaySettings({
|
||||||
|
api,
|
||||||
|
newDisplaySettings,
|
||||||
|
userSettings,
|
||||||
|
userId
|
||||||
|
}: SaveDisplaySettingsParams) {
|
||||||
|
const user = await api.getUser(userId);
|
||||||
|
|
||||||
|
if (appHost.supports('displaylanguage')) {
|
||||||
|
userSettings.language(normalizeValue(newDisplaySettings.language));
|
||||||
|
}
|
||||||
|
userSettings.customCss(normalizeValue(newDisplaySettings.customCss));
|
||||||
|
userSettings.dashboardTheme(normalizeValue(newDisplaySettings.dashboardTheme));
|
||||||
|
userSettings.dateTimeLocale(normalizeValue(newDisplaySettings.dateTimeLocale));
|
||||||
|
userSettings.disableCustomCss(newDisplaySettings.disableCustomCss);
|
||||||
|
userSettings.enableBlurhash(newDisplaySettings.enableBlurHash);
|
||||||
|
userSettings.enableFastFadein(newDisplaySettings.enableFasterAnimation);
|
||||||
|
userSettings.detailsBanner(newDisplaySettings.enableItemDetailsBanner);
|
||||||
|
userSettings.enableBackdrops(newDisplaySettings.enableLibraryBackdrops);
|
||||||
|
userSettings.enableThemeSongs(newDisplaySettings.enableLibraryThemeSongs);
|
||||||
|
userSettings.enableThemeVideos(newDisplaySettings.enableLibraryThemeVideos);
|
||||||
|
userSettings.enableRewatchingInNextUp(newDisplaySettings.enableRewatchingInNextUp);
|
||||||
|
userSettings.useEpisodeImagesInNextUpAndResume(newDisplaySettings.episodeImagesInNextUp);
|
||||||
|
userSettings.libraryPageSize(newDisplaySettings.libraryPageSize);
|
||||||
|
userSettings.maxDaysForNextUp(newDisplaySettings.maxDaysForNextUp);
|
||||||
|
userSettings.screensaver(normalizeValue(newDisplaySettings.screensaver));
|
||||||
|
userSettings.backdropScreensaverInterval(newDisplaySettings.screensaverInterval);
|
||||||
|
userSettings.theme(newDisplaySettings.theme);
|
||||||
|
|
||||||
|
layoutManager.setLayout(normalizeValue(newDisplaySettings.layout));
|
||||||
|
|
||||||
|
const promises = [
|
||||||
|
themeManager.setTheme(userSettings.theme())
|
||||||
|
];
|
||||||
|
|
||||||
|
if (user.Id && user.Configuration) {
|
||||||
|
user.Configuration.DisplayMissingEpisodes = newDisplaySettings.displayMissingEpisodes;
|
||||||
|
promises.push(api.updateUserConfiguration(user.Id, user.Configuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeValue(value: string) {
|
||||||
|
return /^(auto|none)$/.test(value) ? '' : value;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { pluginManager } from 'components/pluginManager';
|
||||||
|
import { Plugin, PluginType } from 'types/plugin';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
export function useScreensavers() {
|
||||||
|
const screensavers = useMemo<Plugin[]>(() => {
|
||||||
|
const installedScreensaverPlugins = pluginManager
|
||||||
|
.ofType(PluginType.Screensaver)
|
||||||
|
.map((plugin: Plugin) => ({
|
||||||
|
...plugin,
|
||||||
|
name: globalize.translate(plugin.name) as string
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'none',
|
||||||
|
name: globalize.translate('None') as string,
|
||||||
|
type: PluginType.Screensaver
|
||||||
|
},
|
||||||
|
...installedScreensaverPlugins
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
screensavers: screensavers ?? []
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import themeManager from 'scripts/themeManager';
|
||||||
|
import { Theme } from 'types/webConfig';
|
||||||
|
|
||||||
|
export function useServerThemes() {
|
||||||
|
const [themes, setThemes] = useState<Theme[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getServerThemes() {
|
||||||
|
const loadedThemes = await themeManager.getThemes();
|
||||||
|
|
||||||
|
setThemes(loadedThemes ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!themes) {
|
||||||
|
void getServerThemes();
|
||||||
|
}
|
||||||
|
// We've intentionally left the dependency array here to ensure that the effect happens only once.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const defaultTheme = useMemo(() => {
|
||||||
|
if (!themes) return null;
|
||||||
|
return themes.find((theme) => theme.default);
|
||||||
|
}, [themes]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
themes: themes ?? [],
|
||||||
|
defaultTheme
|
||||||
|
};
|
||||||
|
}
|
96
src/apps/experimental/routes/user/display/index.tsx
Normal file
96
src/apps/experimental/routes/user/display/index.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { DisplayPreferences } from './DisplayPreferences';
|
||||||
|
import { ItemDetailPreferences } from './ItemDetailPreferences';
|
||||||
|
import { LibraryPreferences } from './LibraryPreferences';
|
||||||
|
import { LocalizationPreferences } from './LocalizationPreferences';
|
||||||
|
import { NextUpPreferences } from './NextUpPreferences';
|
||||||
|
import { useDisplaySettingForm } from './hooks/useDisplaySettingForm';
|
||||||
|
import { DisplaySettingsValues } from './types';
|
||||||
|
import LoadingComponent from 'components/loading/LoadingComponent';
|
||||||
|
|
||||||
|
export default function UserDisplayPreferences() {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
submitChanges,
|
||||||
|
updateField,
|
||||||
|
values
|
||||||
|
} = useDisplaySettingForm();
|
||||||
|
|
||||||
|
const handleSubmitForm = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void submitChanges();
|
||||||
|
}, [submitChanges]);
|
||||||
|
|
||||||
|
const handleFieldChange = useCallback((e: SelectChangeEvent | React.SyntheticEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const fieldName = target.name as keyof DisplaySettingsValues;
|
||||||
|
const fieldValue = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
|
|
||||||
|
if (values?.[fieldName] !== fieldValue) {
|
||||||
|
updateField({
|
||||||
|
name: fieldName,
|
||||||
|
value: fieldValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [updateField, values]);
|
||||||
|
|
||||||
|
if (loading || !values) {
|
||||||
|
return <LoadingComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
className='libraryPage userPreferencesPage noSecondaryNavPage'
|
||||||
|
id='displayPreferencesPage'
|
||||||
|
title={globalize.translate('Display')}
|
||||||
|
>
|
||||||
|
<div className='settingsContainer padded-left padded-right padded-bottom-page'>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmitForm}
|
||||||
|
style={{ margin: 'auto' }}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<LocalizationPreferences
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
<DisplayPreferences
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
<LibraryPreferences
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
<NextUpPreferences
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
<ItemDetailPreferences
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: theme.typography.htmlFontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightBold
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{globalize.translate('Save')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
22
src/apps/experimental/routes/user/display/types.ts
Normal file
22
src/apps/experimental/routes/user/display/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export interface DisplaySettingsValues {
|
||||||
|
customCss: string;
|
||||||
|
dashboardTheme: string;
|
||||||
|
dateTimeLocale: string;
|
||||||
|
disableCustomCss: boolean;
|
||||||
|
displayMissingEpisodes: boolean;
|
||||||
|
enableBlurHash: boolean;
|
||||||
|
enableFasterAnimation: boolean;
|
||||||
|
enableItemDetailsBanner: boolean;
|
||||||
|
enableLibraryBackdrops: boolean;
|
||||||
|
enableLibraryThemeSongs: boolean;
|
||||||
|
enableLibraryThemeVideos: boolean;
|
||||||
|
enableRewatchingInNextUp: boolean;
|
||||||
|
episodeImagesInNextUp: boolean;
|
||||||
|
language: string;
|
||||||
|
layout: string;
|
||||||
|
libraryPageSize: number;
|
||||||
|
maxDaysForNextUp: number;
|
||||||
|
screensaver: string;
|
||||||
|
screensaverInterval: number;
|
||||||
|
theme: string;
|
||||||
|
}
|
|
@ -104,6 +104,18 @@ class ServerConnections extends ConnectionManager {
|
||||||
return apiClient;
|
return apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ApiClient that is currently connected or throws if not defined.
|
||||||
|
* @async
|
||||||
|
* @returns {Promise<ApiClient>} The current ApiClient instance.
|
||||||
|
*/
|
||||||
|
async getCurrentApiClientAsync() {
|
||||||
|
const apiClient = this.currentApiClient();
|
||||||
|
if (!apiClient) throw new Error('[ServerConnection] No current ApiClient instance');
|
||||||
|
|
||||||
|
return apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
onLocalUserSignedIn(user) {
|
onLocalUserSignedIn(user) {
|
||||||
const apiClient = this.getApiClient(user.ServerId);
|
const apiClient = this.getApiClient(user.ServerId);
|
||||||
this.setLocalApiClient(apiClient);
|
this.setLocalApiClient(apiClient);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import Package from '../../package.json';
|
|
||||||
import appSettings from '../scripts/settings/appSettings';
|
import appSettings from '../scripts/settings/appSettings';
|
||||||
import browser from '../scripts/browser';
|
import browser from '../scripts/browser';
|
||||||
import Events from '../utils/events.ts';
|
import Events from '../utils/events.ts';
|
||||||
|
@ -36,7 +35,7 @@ function getDeviceProfile(item) {
|
||||||
let profile;
|
let profile;
|
||||||
|
|
||||||
if (window.NativeShell) {
|
if (window.NativeShell) {
|
||||||
profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder, Package.version);
|
profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder, __PACKAGE_JSON_VERSION__);
|
||||||
} else {
|
} else {
|
||||||
const builderOpts = getBaseProfileOptions(item);
|
const builderOpts = getBaseProfileOptions(item);
|
||||||
profile = profileBuilder(builderOpts);
|
profile = profileBuilder(builderOpts);
|
||||||
|
@ -46,18 +45,27 @@ function getDeviceProfile(item) {
|
||||||
const maxTranscodingVideoWidth = maxVideoWidth < 0 ? appHost.screen()?.maxAllowedWidth : maxVideoWidth;
|
const maxTranscodingVideoWidth = maxVideoWidth < 0 ? appHost.screen()?.maxAllowedWidth : maxVideoWidth;
|
||||||
|
|
||||||
if (maxTranscodingVideoWidth) {
|
if (maxTranscodingVideoWidth) {
|
||||||
|
const conditionWidth = {
|
||||||
|
Condition: 'LessThanEqual',
|
||||||
|
Property: 'Width',
|
||||||
|
Value: maxTranscodingVideoWidth.toString(),
|
||||||
|
IsRequired: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appSettings.limitSupportedVideoResolution()) {
|
||||||
|
profile.CodecProfiles.push({
|
||||||
|
Type: 'Video',
|
||||||
|
Conditions: [conditionWidth]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
profile.TranscodingProfiles.forEach((transcodingProfile) => {
|
profile.TranscodingProfiles.forEach((transcodingProfile) => {
|
||||||
if (transcodingProfile.Type === 'Video') {
|
if (transcodingProfile.Type === 'Video') {
|
||||||
transcodingProfile.Conditions = (transcodingProfile.Conditions || []).filter((condition) => {
|
transcodingProfile.Conditions = (transcodingProfile.Conditions || []).filter((condition) => {
|
||||||
return condition.Property !== 'Width';
|
return condition.Property !== 'Width';
|
||||||
});
|
});
|
||||||
|
|
||||||
transcodingProfile.Conditions.push({
|
transcodingProfile.Conditions.push(conditionWidth);
|
||||||
Condition: 'LessThanEqual',
|
|
||||||
Property: 'Width',
|
|
||||||
Value: maxTranscodingVideoWidth.toString(),
|
|
||||||
IsRequired: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -378,7 +386,7 @@ export const appHost = {
|
||||||
},
|
},
|
||||||
appVersion: function () {
|
appVersion: function () {
|
||||||
return window.NativeShell?.AppHost?.appVersion ?
|
return window.NativeShell?.AppHost?.appVersion ?
|
||||||
window.NativeShell.AppHost.appVersion() : Package.version;
|
window.NativeShell.AppHost.appVersion() : __PACKAGE_JSON_VERSION__;
|
||||||
},
|
},
|
||||||
getPushTokenInfo: function () {
|
getPushTokenInfo: function () {
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import layoutManager from 'components/layoutManager';
|
||||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||||
import type { ItemDto } from 'types/base/models/item-dto';
|
import type { ItemDto } from 'types/base/models/item-dto';
|
||||||
import type { CardOptions } from 'types/cardOptions';
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
import Image from 'components/common/Image';
|
||||||
|
|
||||||
const shouldShowDetailsMenu = (
|
const shouldShowDetailsMenu = (
|
||||||
cardOptions: CardOptions,
|
cardOptions: CardOptions,
|
||||||
|
@ -23,9 +24,14 @@ interface LogoComponentProps {
|
||||||
logoUrl: string;
|
logoUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogoComponent: FC<LogoComponentProps> = ({ logoUrl }) => {
|
const LogoComponent: FC<LogoComponentProps> = ({ logoUrl }) => (
|
||||||
return <Box className='lazy cardFooterLogo' data-src={logoUrl} />;
|
<Box className='cardFooterLogo'>
|
||||||
};
|
<Image
|
||||||
|
imgUrl={logoUrl}
|
||||||
|
containImage
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
interface CardFooterTextProps {
|
interface CardFooterTextProps {
|
||||||
item: ItemDto;
|
item: ItemDto;
|
||||||
|
@ -51,7 +57,7 @@ const CardFooterText: FC<CardFooterTextProps> = ({
|
||||||
isOuterFooter
|
isOuterFooter
|
||||||
}) => {
|
}) => {
|
||||||
const { cardTextLines } = useCardText({
|
const { cardTextLines } = useCardText({
|
||||||
item,
|
item: item.ProgramInfo || item,
|
||||||
cardOptions,
|
cardOptions,
|
||||||
forceName,
|
forceName,
|
||||||
imgUrl,
|
imgUrl,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
} from '@jellyfin/sdk/lib/generated-client';
|
} from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { Api } from '@jellyfin/sdk';
|
import { Api } from '@jellyfin/sdk';
|
||||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||||
import escapeHTML from 'escape-html';
|
|
||||||
|
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import layoutManager from 'components/layoutManager';
|
import layoutManager from 'components/layoutManager';
|
||||||
|
@ -78,15 +77,11 @@ export function getTextActionButton(
|
||||||
text?: NullableString,
|
text?: NullableString,
|
||||||
serverId?: NullableString
|
serverId?: NullableString
|
||||||
): TextLine {
|
): TextLine {
|
||||||
if (!text) {
|
const title = text || itemHelper.getDisplayName(item);
|
||||||
text = itemHelper.getDisplayName(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
text = escapeHTML(text);
|
|
||||||
|
|
||||||
if (layoutManager.tv) {
|
if (layoutManager.tv) {
|
||||||
return {
|
return {
|
||||||
title: text
|
title
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +103,7 @@ export function getTextActionButton(
|
||||||
return {
|
return {
|
||||||
titleAction: {
|
titleAction: {
|
||||||
url,
|
url,
|
||||||
title: text,
|
title,
|
||||||
dataAttributes
|
dataAttributes
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -510,7 +505,7 @@ function getChannelName(item: ItemDto) {
|
||||||
item.ChannelName
|
item.ChannelName
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return { title: item.ChannelName || '' || ' ' };
|
return { title: item.ChannelName || '\u00A0' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { type FunctionComponent } from 'react';
|
||||||
|
import globalize from '../../../../scripts/globalize';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
href?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLinkElement = ({ className, title, href }: IProps) => ({
|
||||||
|
__html: `<a
|
||||||
|
is="emby-linkbutton"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="${className}"
|
||||||
|
href="${href}"
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
</a>`
|
||||||
|
});
|
||||||
|
|
||||||
|
const LinkTrickplayAcceleration: FunctionComponent<IProps> = ({ className, title, href }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createLinkElement({
|
||||||
|
className,
|
||||||
|
title: globalize.translate(title),
|
||||||
|
href
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkTrickplayAcceleration;
|
|
@ -2,10 +2,11 @@ import React, { FunctionComponent } from 'react';
|
||||||
import IconButtonElement from '../../../elements/IconButtonElement';
|
import IconButtonElement from '../../../elements/IconButtonElement';
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
tag?: string;
|
tag?: string,
|
||||||
|
tagType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
const TagList: FunctionComponent<IProps> = ({ tag, tagType }: IProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='paperList'>
|
<div className='paperList'>
|
||||||
<div className='listItem'>
|
<div className='listItem'>
|
||||||
|
@ -16,7 +17,7 @@ const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
||||||
</div>
|
</div>
|
||||||
<IconButtonElement
|
<IconButtonElement
|
||||||
is='paper-icon-button-light'
|
is='paper-icon-button-light'
|
||||||
className='blockedTag btnDeleteTag listItemButton'
|
className={`${tagType} btnDeleteTag listItemButton`}
|
||||||
title='Delete'
|
title='Delete'
|
||||||
icon='delete'
|
icon='delete'
|
||||||
dataTag={tag}
|
dataTag={tag}
|
||||||
|
@ -26,4 +27,4 @@ const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BlockedTagList;
|
export default TagList;
|
|
@ -61,7 +61,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-userid={user.Id} className={cssClass}>
|
<div data-userid={user.Id} data-username={user.Name} className={cssClass}>
|
||||||
<div className='cardBox visualCardBox'>
|
<div className='cardBox visualCardBox'>
|
||||||
<div className='cardScalable visualCardBox-cardScalable'>
|
<div className='cardScalable visualCardBox-cardScalable'>
|
||||||
<div className='cardPadder cardPadder-square'></div>
|
<div className='cardPadder cardPadder-square'></div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import appSettings from '../scripts/settings/appSettings' ;
|
import appSettings from '../scripts/settings/appSettings' ;
|
||||||
import browser from '../scripts/browser';
|
import browser from '../scripts/browser';
|
||||||
import Events from '../utils/events.ts';
|
import Events from '../utils/events.ts';
|
||||||
|
import { MediaError } from 'types/mediaError';
|
||||||
|
|
||||||
export function getSavedVolume() {
|
export function getSavedVolume() {
|
||||||
return appSettings.get('volume') || 1;
|
return appSettings.get('volume') || 1;
|
||||||
|
@ -87,7 +88,7 @@ export function handleHlsJsMediaError(instance, reject) {
|
||||||
if (reject) {
|
if (reject) {
|
||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
onErrorInternal(instance, 'mediadecodeerror');
|
onErrorInternal(instance, MediaError.FATAL_HLS_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,11 +99,7 @@ export function onErrorInternal(instance, type) {
|
||||||
instance.destroyCustomTrack(instance._mediaElement);
|
instance.destroyCustomTrack(instance._mediaElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.trigger(instance, 'error', [
|
Events.trigger(instance, 'error', [{ type }]);
|
||||||
{
|
|
||||||
type: type
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidDuration(duration) {
|
export function isValidDuration(duration) {
|
||||||
|
@ -193,7 +190,7 @@ export function playWithPromise(elem, onErrorFn) {
|
||||||
// swallow this error because the user can still click the play button on the video element
|
// swallow this error because the user can still click the play button on the video element
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject();
|
return Promise.reject(e);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccessfulPlay(elem, onErrorFn);
|
onSuccessfulPlay(elem, onErrorFn);
|
||||||
|
@ -269,10 +266,10 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
|
|
||||||
if (reject) {
|
if (reject) {
|
||||||
reject('servererror');
|
reject(MediaError.SERVER_ERROR);
|
||||||
reject = null;
|
reject = null;
|
||||||
} else {
|
} else {
|
||||||
onErrorInternal(instance, 'servererror');
|
onErrorInternal(instance, MediaError.SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -291,10 +288,10 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
|
|
||||||
if (reject) {
|
if (reject) {
|
||||||
reject('network');
|
reject(MediaError.NETWORK_ERROR);
|
||||||
reject = null;
|
reject = null;
|
||||||
} else {
|
} else {
|
||||||
onErrorInternal(instance, 'network');
|
onErrorInternal(instance, MediaError.NETWORK_ERROR);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.debug('fatal network error encountered, try to recover');
|
console.debug('fatal network error encountered, try to recover');
|
||||||
|
@ -318,7 +315,7 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
||||||
reject();
|
reject();
|
||||||
reject = null;
|
reject = null;
|
||||||
} else {
|
} else {
|
||||||
onErrorInternal(instance, 'mediadecodeerror');
|
onErrorInternal(instance, MediaError.FATAL_HLS_ERROR);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,24 @@ import { playbackManager } from './playback/playbackmanager';
|
||||||
import ServerConnections from './ServerConnections';
|
import ServerConnections from './ServerConnections';
|
||||||
import toast from './toast/toast';
|
import toast from './toast/toast';
|
||||||
import * as userSettings from '../scripts/settings/userSettings';
|
import * as userSettings from '../scripts/settings/userSettings';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
|
||||||
|
function getDeleteLabel(type) {
|
||||||
|
switch (type) {
|
||||||
|
case BaseItemKind.Series:
|
||||||
|
return globalize.translate('DeleteSeries');
|
||||||
|
|
||||||
|
case BaseItemKind.Episode:
|
||||||
|
return globalize.translate('DeleteEpisode');
|
||||||
|
|
||||||
|
case BaseItemKind.Playlist:
|
||||||
|
case BaseItemKind.BoxSet:
|
||||||
|
return globalize.translate('Delete');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return globalize.translate('DeleteMedia');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getCommands(options) {
|
export function getCommands(options) {
|
||||||
const item = options.item;
|
const item = options.item;
|
||||||
|
@ -160,19 +178,11 @@ export function getCommands(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.CanDelete && options.deleteItem !== false) {
|
if (item.CanDelete && options.deleteItem !== false) {
|
||||||
if (item.Type === 'Playlist' || item.Type === 'BoxSet') {
|
|
||||||
commands.push({
|
commands.push({
|
||||||
name: globalize.translate('Delete'),
|
name: getDeleteLabel(item.Type),
|
||||||
id: 'delete',
|
id: 'delete',
|
||||||
icon: 'delete'
|
icon: 'delete'
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
commands.push({
|
|
||||||
name: globalize.translate('DeleteMedia'),
|
|
||||||
id: 'delete',
|
|
||||||
icon: 'delete'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Books are promoted to major download Button and therefor excluded in the context menu
|
// Books are promoted to major download Button and therefor excluded in the context menu
|
||||||
|
@ -214,11 +224,7 @@ export function getCommands(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEdit && item.MediaType === 'Video' && item.Type !== 'TvChannel' && item.Type !== 'Program'
|
if (itemHelper.canEditSubtitles(user, item) && options.editSubtitles !== false) {
|
||||||
&& item.LocationType !== 'Virtual'
|
|
||||||
&& !(item.Type === 'Recording' && item.Status !== 'Completed')
|
|
||||||
&& options.editSubtitles !== false
|
|
||||||
) {
|
|
||||||
commands.push({
|
commands.push({
|
||||||
name: globalize.translate('EditSubtitles'),
|
name: globalize.translate('EditSubtitles'),
|
||||||
id: 'editsubtitles',
|
id: 'editsubtitles',
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { appHost } from './apphost';
|
import { appHost } from './apphost';
|
||||||
import globalize from '../scripts/globalize';
|
import globalize from '../scripts/globalize';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
||||||
|
import { RecordingStatus } from '@jellyfin/sdk/lib/generated-client/models/recording-status';
|
||||||
|
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
||||||
|
|
||||||
export function getDisplayName(item, options = {}) {
|
export function getDisplayName(item, options = {}) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -155,6 +159,33 @@ export function canEditImages (user, item) {
|
||||||
return itemType !== 'Timer' && itemType !== 'SeriesTimer' && canEdit(user, item) && !isLocalItem(item);
|
return itemType !== 'Timer' && itemType !== 'SeriesTimer' && canEdit(user, item) && !isLocalItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canEditSubtitles (user, item) {
|
||||||
|
if (item.MediaType !== MediaType.Video) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const itemType = item.Type;
|
||||||
|
if (itemType === BaseItemKind.Recording && item.Status !== RecordingStatus.Completed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (itemType === BaseItemKind.TvChannel
|
||||||
|
|| itemType === BaseItemKind.Program
|
||||||
|
|| itemType === 'Timer'
|
||||||
|
|| itemType === 'SeriesTimer'
|
||||||
|
|| itemType === BaseItemKind.UserRootFolder
|
||||||
|
|| itemType === BaseItemKind.UserView
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isLocalItem(item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item.LocationType === LocationType.Virtual) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.Policy.EnableSubtitleManagement
|
||||||
|
|| user.Policy.IsAdministrator;
|
||||||
|
}
|
||||||
|
|
||||||
export function canShare (item, user) {
|
export function canShare (item, user) {
|
||||||
if (item.Type === 'Program') {
|
if (item.Type === 'Program') {
|
||||||
return false;
|
return false;
|
||||||
|
@ -300,6 +331,7 @@ export default {
|
||||||
canIdentify: canIdentify,
|
canIdentify: canIdentify,
|
||||||
canEdit: canEdit,
|
canEdit: canEdit,
|
||||||
canEditImages: canEditImages,
|
canEditImages: canEditImages,
|
||||||
|
canEditSubtitles,
|
||||||
canShare: canShare,
|
canShare: canShare,
|
||||||
enableDateAddedDisplay: enableDateAddedDisplay,
|
enableDateAddedDisplay: enableDateAddedDisplay,
|
||||||
canMarkPlayed: canMarkPlayed,
|
canMarkPlayed: canMarkPlayed,
|
||||||
|
|
|
@ -84,6 +84,7 @@ function getMediaSourceHtml(user, item, version) {
|
||||||
case 'Data':
|
case 'Data':
|
||||||
case 'Subtitle':
|
case 'Subtitle':
|
||||||
case 'Video':
|
case 'Video':
|
||||||
|
case 'Lyric':
|
||||||
translateString = stream.Type;
|
translateString = stream.Type;
|
||||||
break;
|
break;
|
||||||
case 'EmbeddedImage':
|
case 'EmbeddedImage':
|
||||||
|
@ -145,10 +146,10 @@ function getMediaSourceHtml(user, item, version) {
|
||||||
if (stream.BitDepth) {
|
if (stream.BitDepth) {
|
||||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
|
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
|
||||||
}
|
}
|
||||||
if (stream.VideoRange) {
|
if (stream.VideoRange && stream.Type === 'Video') {
|
||||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
|
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
|
||||||
}
|
}
|
||||||
if (stream.VideoRangeType) {
|
if (stream.VideoRangeType && stream.Type === 'Video') {
|
||||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
|
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
|
||||||
}
|
}
|
||||||
if (stream.VideoDoViTitle) {
|
if (stream.VideoDoViTitle) {
|
||||||
|
|
|
@ -391,8 +391,10 @@ export function setContentType(parent, contentType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType !== 'tvshows' && contentType !== 'movies' && contentType !== 'homevideos' && contentType !== 'musicvideos' && contentType !== 'mixed') {
|
if (contentType !== 'tvshows' && contentType !== 'movies' && contentType !== 'homevideos' && contentType !== 'musicvideos' && contentType !== 'mixed') {
|
||||||
|
parent.querySelector('.trickplaySettingsSection').classList.add('hide');
|
||||||
parent.querySelector('.chapterSettingsSection').classList.add('hide');
|
parent.querySelector('.chapterSettingsSection').classList.add('hide');
|
||||||
} else {
|
} else {
|
||||||
|
parent.querySelector('.trickplaySettingsSection').classList.remove('hide');
|
||||||
parent.querySelector('.chapterSettingsSection').classList.remove('hide');
|
parent.querySelector('.chapterSettingsSection').classList.remove('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +418,8 @@ export function setContentType(parent, contentType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parent.querySelector('.chkUseReplayGainTagsContainer').classList.toggle('hide', contentType !== 'music');
|
||||||
|
|
||||||
parent.querySelector('.chkEnableLUFSScanContainer').classList.toggle('hide', contentType !== 'music');
|
parent.querySelector('.chkEnableLUFSScanContainer').classList.toggle('hide', contentType !== 'music');
|
||||||
|
|
||||||
if (contentType === 'tvshows') {
|
if (contentType === 'tvshows') {
|
||||||
|
@ -511,10 +515,14 @@ function setImageOptionsIntoOptions(options) {
|
||||||
|
|
||||||
export function getLibraryOptions(parent) {
|
export function getLibraryOptions(parent) {
|
||||||
const options = {
|
const options = {
|
||||||
|
Enabled: parent.querySelector('.chkEnabled').checked,
|
||||||
EnableArchiveMediaFiles: false,
|
EnableArchiveMediaFiles: false,
|
||||||
EnablePhotos: parent.querySelector('.chkEnablePhotos').checked,
|
EnablePhotos: parent.querySelector('.chkEnablePhotos').checked,
|
||||||
EnableRealtimeMonitor: parent.querySelector('.chkEnableRealtimeMonitor').checked,
|
EnableRealtimeMonitor: parent.querySelector('.chkEnableRealtimeMonitor').checked,
|
||||||
EnableLUFSScan: parent.querySelector('.chkEnableLUFSScan').checked,
|
EnableLUFSScan: parent.querySelector('.chkEnableLUFSScan').checked,
|
||||||
|
ExtractTrickplayImagesDuringLibraryScan: parent.querySelector('.chkExtractTrickplayDuringLibraryScan').checked,
|
||||||
|
EnableTrickplayImageExtraction: parent.querySelector('.chkExtractTrickplayImages').checked,
|
||||||
|
UseReplayGainTags: parent.querySelector('.chkUseReplayGainTags').checked,
|
||||||
ExtractChapterImagesDuringLibraryScan: parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked,
|
ExtractChapterImagesDuringLibraryScan: parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked,
|
||||||
EnableChapterImageExtraction: parent.querySelector('.chkExtractChapterImages').checked,
|
EnableChapterImageExtraction: parent.querySelector('.chkExtractChapterImages').checked,
|
||||||
EnableInternetProviders: true,
|
EnableInternetProviders: true,
|
||||||
|
@ -574,9 +582,13 @@ export function setLibraryOptions(parent, options) {
|
||||||
parent.querySelector('#selectCountry').value = options.MetadataCountryCode || '';
|
parent.querySelector('#selectCountry').value = options.MetadataCountryCode || '';
|
||||||
parent.querySelector('#selectAutoRefreshInterval').value = options.AutomaticRefreshIntervalDays || '0';
|
parent.querySelector('#selectAutoRefreshInterval').value = options.AutomaticRefreshIntervalDays || '0';
|
||||||
parent.querySelector('#txtSeasonZeroName').value = options.SeasonZeroDisplayName || 'Specials';
|
parent.querySelector('#txtSeasonZeroName').value = options.SeasonZeroDisplayName || 'Specials';
|
||||||
|
parent.querySelector('.chkEnabled').checked = options.Enabled;
|
||||||
parent.querySelector('.chkEnablePhotos').checked = options.EnablePhotos;
|
parent.querySelector('.chkEnablePhotos').checked = options.EnablePhotos;
|
||||||
parent.querySelector('.chkEnableRealtimeMonitor').checked = options.EnableRealtimeMonitor;
|
parent.querySelector('.chkEnableRealtimeMonitor').checked = options.EnableRealtimeMonitor;
|
||||||
parent.querySelector('.chkEnableLUFSScan').checked = options.EnableLUFSScan;
|
parent.querySelector('.chkEnableLUFSScan').checked = options.EnableLUFSScan;
|
||||||
|
parent.querySelector('.chkExtractTrickplayDuringLibraryScan').checked = options.ExtractTrickplayImagesDuringLibraryScan;
|
||||||
|
parent.querySelector('.chkExtractTrickplayImages').checked = options.EnableTrickplayImageExtraction;
|
||||||
|
parent.querySelector('.chkUseReplayGainTags').checked = options.UseReplayGainTags;
|
||||||
parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked = options.ExtractChapterImagesDuringLibraryScan;
|
parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked = options.ExtractChapterImagesDuringLibraryScan;
|
||||||
parent.querySelector('.chkExtractChapterImages').checked = options.EnableChapterImageExtraction;
|
parent.querySelector('.chkExtractChapterImages').checked = options.EnableChapterImageExtraction;
|
||||||
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
|
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
<h2>${HeaderLibrarySettings}</h2>
|
<h2>${HeaderLibrarySettings}</h2>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription chkEnabledContainer">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkEnabled" checked />
|
||||||
|
<span>${EnableLibrary}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${EnableLibraryHelp}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="selectContainer fldMetadataLanguage hide">
|
<div class="selectContainer fldMetadataLanguage hide">
|
||||||
<select is="emby-select" id="selectLanguage" label="${LabelMetadataDownloadLanguage}"></select>
|
<select is="emby-select" id="selectLanguage" label="${LabelMetadataDownloadLanguage}"></select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,6 +63,14 @@
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableRealtimeMonitorHelp}</div>
|
<div class="fieldDescription checkboxFieldDescription">${LabelEnableRealtimeMonitorHelp}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription chkUseReplayGainTagsContainer advanced">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkUseReplayGainTags" checked />
|
||||||
|
<span>${LabelUseReplayGainTags}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${LabelUseReplayGainTagsHelp}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription chkEnableLUFSScanContainer advanced">
|
<div class="checkboxContainer checkboxContainer-withDescription chkEnableLUFSScanContainer advanced">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkEnableLUFSScan" checked />
|
<input type="checkbox" is="emby-checkbox" class="chkEnableLUFSScan" checked />
|
||||||
|
@ -104,6 +120,25 @@
|
||||||
<div class="fieldDescription checkboxFieldDescription">${OptionAutomaticallyGroupSeriesHelp}</div>
|
<div class="fieldDescription checkboxFieldDescription">${OptionAutomaticallyGroupSeriesHelp}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="trickplaySettingsSection hide">
|
||||||
|
<h2>${Trickplay}</h2>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription fldExtractTrickplayImages">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkExtractTrickplayImages" />
|
||||||
|
<span>${OptionExtractTrickplayImage}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${ExtractTrickplayImagesHelp}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription fldExtractTrickplayDuringLibraryScan advanced">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkExtractTrickplayDuringLibraryScan" />
|
||||||
|
<span>${LabelExtractTrickplayDuringLibraryScan}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${LabelExtractTrickplayDuringLibraryScanHelp}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chapterSettingsSection hide">
|
<div class="chapterSettingsSection hide">
|
||||||
<h2>${HeaderChapterImages}</h2>
|
<h2>${HeaderChapterImages}</h2>
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription fldExtractChapterImages">
|
<div class="checkboxContainer checkboxContainer-withDescription fldExtractChapterImages">
|
||||||
|
|
|
@ -22,6 +22,7 @@ import ServerConnections from '../ServerConnections';
|
||||||
import toast from '../toast/toast';
|
import toast from '../toast/toast';
|
||||||
import { appRouter } from '../router/appRouter';
|
import { appRouter } from '../router/appRouter';
|
||||||
import template from './metadataEditor.template.html';
|
import template from './metadataEditor.template.html';
|
||||||
|
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
let currentContext;
|
let currentContext;
|
||||||
let metadataEditorInfo;
|
let metadataEditorInfo;
|
||||||
|
@ -271,7 +272,7 @@ function showMoreMenu(context, button, user) {
|
||||||
} else if (result.updated) {
|
} else if (result.updated) {
|
||||||
reload(context, item.Id, item.ServerId);
|
reload(context, item.Id, item.ServerId);
|
||||||
}
|
}
|
||||||
});
|
}).catch(() => { /* no-op */ });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,10 +887,10 @@ function populateRatings(allParentalRatings, select, currentValue) {
|
||||||
|
|
||||||
function populateStatus(select) {
|
function populateStatus(select) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
html += '<option value=""></option>';
|
||||||
html += "<option value=''></option>";
|
html += `<option value="${SeriesStatus.Continuing}">${escapeHtml(globalize.translate('Continuing'))}</option>`;
|
||||||
html += "<option value='Continuing'>" + globalize.translate('Continuing') + '</option>';
|
html += `<option value="${SeriesStatus.Ended}">${escapeHtml(globalize.translate('Ended'))}</option>`;
|
||||||
html += "<option value='Ended'>" + globalize.translate('Ended') + '</option>';
|
html += `<option value="${SeriesStatus.Unreleased}">${escapeHtml(globalize.translate('Unreleased'))}</option>`;
|
||||||
select.innerHTML = html;
|
select.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { playbackManager } from '../playback/playbackmanager';
|
||||||
import nowPlayingHelper from '../playback/nowplayinghelper';
|
import nowPlayingHelper from '../playback/nowplayinghelper';
|
||||||
import { appHost } from '../apphost';
|
import { appHost } from '../apphost';
|
||||||
import dom from '../../scripts/dom';
|
import dom from '../../scripts/dom';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
import itemContextMenu from '../itemContextMenu';
|
import itemContextMenu from '../itemContextMenu';
|
||||||
import '../../elements/emby-button/paper-icon-button-light';
|
import '../../elements/emby-button/paper-icon-button-light';
|
||||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||||
|
@ -59,13 +60,13 @@ function getNowPlayingBarHtml() {
|
||||||
// The onclicks are needed due to the return false above
|
// The onclicks are needed due to the return false above
|
||||||
html += '<div class="nowPlayingBarCenter" dir="ltr">';
|
html += '<div class="nowPlayingBarCenter" dir="ltr">';
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="previousTrackButton mediaButton"><span class="material-icons skip_previous" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="previousTrackButton mediaButton" title="${globalize.translate('ButtonPreviousTrack')}"><span class="material-icons skip_previous" aria-hidden="true"></span></button>`;
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="playPauseButton mediaButton" title="${globalize.translate('ButtonPause')}"><span class="material-icons pause" aria-hidden="true"></span></button>`;
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="stopButton mediaButton"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="stopButton mediaButton" title="${globalize.translate('ButtonStop')}"><span class="material-icons stop" aria-hidden="true"></span></button>`;
|
||||||
if (!layoutManager.mobile) {
|
if (!layoutManager.mobile) {
|
||||||
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="nextTrackButton mediaButton" title="${globalize.translate('ButtonNextTrack')}"><span class="material-icons skip_next" aria-hidden="true"></span></button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '<div class="nowPlayingBarCurrentTime"></div>';
|
html += '<div class="nowPlayingBarCurrentTime"></div>';
|
||||||
|
@ -73,25 +74,25 @@ function getNowPlayingBarHtml() {
|
||||||
|
|
||||||
html += '<div class="nowPlayingBarRight">';
|
html += '<div class="nowPlayingBarRight">';
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="muteButton mediaButton"><span class="material-icons volume_up" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="muteButton mediaButton" title="${globalize.translate('Mute')}"><span class="material-icons volume_up" aria-hidden="true"></span></button>`;
|
||||||
|
|
||||||
html += '<div class="sliderContainer nowPlayingBarVolumeSliderContainer hide" style="width:9em;vertical-align:middle;display:inline-flex;">';
|
html += '<div class="sliderContainer nowPlayingBarVolumeSliderContainer hide" style="width:9em;vertical-align:middle;display:inline-flex;">';
|
||||||
html += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>';
|
html += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="btnAirPlay mediaButton"><span class="material-icons airplay" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="btnAirPlay mediaButton" title="${globalize.translate('AirPlay')}"><span class="material-icons airplay" aria-hidden="true"></span></button>`;
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton"><span class="material-icons repeat" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton" title="${globalize.translate('Repeat')}"><span class="material-icons repeat" aria-hidden="true"></span></button>`;
|
||||||
html += '<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton"><span class="material-icons shuffle" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton" title="${globalize.translate('Shuffle')}"><span class="material-icons shuffle" aria-hidden="true"></span></button>`;
|
||||||
|
|
||||||
html += '<div class="nowPlayingBarUserDataButtons">';
|
html += '<div class="nowPlayingBarUserDataButtons">';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="playPauseButton mediaButton" title="${globalize.translate('ButtonPause')}"><span class="material-icons pause" aria-hidden="true"></span></button>`;
|
||||||
if (layoutManager.mobile) {
|
if (layoutManager.mobile) {
|
||||||
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="nextTrackButton mediaButton" title="${globalize.translate('ButtonNextTrack')}"><span class="material-icons skip_next" aria-hidden="true"></span></button>`;
|
||||||
} else {
|
} else {
|
||||||
html += '<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
html += `<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton" title="${globalize.translate('ButtonMore')}"><span class="material-icons more_vert" aria-hidden="true"></span></button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
@ -317,6 +318,7 @@ function updatePlayPauseState(isPaused) {
|
||||||
const icon = button.querySelector('.material-icons');
|
const icon = button.querySelector('.material-icons');
|
||||||
icon.classList.remove('play_arrow', 'pause');
|
icon.classList.remove('play_arrow', 'pause');
|
||||||
icon.classList.add(isPaused ? 'play_arrow' : 'pause');
|
icon.classList.add(isPaused ? 'play_arrow' : 'pause');
|
||||||
|
button.title = globalize.translate(isPaused ? 'Play' : 'ButtonPause');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,6 +426,7 @@ function updatePlayerVolumeState(isMuted, volumeLevel) {
|
||||||
const muteButtonIcon = muteButton.querySelector('.material-icons');
|
const muteButtonIcon = muteButton.querySelector('.material-icons');
|
||||||
muteButtonIcon.classList.remove('volume_off', 'volume_up');
|
muteButtonIcon.classList.remove('volume_off', 'volume_up');
|
||||||
muteButtonIcon.classList.add(isMuted ? 'volume_off' : 'volume_up');
|
muteButtonIcon.classList.add(isMuted ? 'volume_off' : 'volume_up');
|
||||||
|
muteButton.title = globalize.translate(isMuted ? 'Unmute' : 'Mute');
|
||||||
|
|
||||||
if (supportedCommands.indexOf('SetVolume') === -1) {
|
if (supportedCommands.indexOf('SetVolume') === -1) {
|
||||||
showVolumeSlider = false;
|
showVolumeSlider = false;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { PlaybackErrorCode } from '@jellyfin/sdk/lib/generated-client/models/playback-error-code.js';
|
||||||
|
import merge from 'lodash-es/merge';
|
||||||
|
import Screenfull from 'screenfull';
|
||||||
|
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
import datetime from '../../scripts/datetime';
|
import datetime from '../../scripts/datetime';
|
||||||
import appSettings from '../../scripts/settings/appSettings';
|
import appSettings from '../../scripts/settings/appSettings';
|
||||||
|
@ -8,14 +12,15 @@ 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 Screenfull from 'screenfull';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
import alert from '../alert';
|
import alert from '../alert';
|
||||||
import { PluginType } from '../../types/plugin.ts';
|
import { PluginType } from '../../types/plugin.ts';
|
||||||
import { includesAny } from '../../utils/container.ts';
|
import { includesAny } from '../../utils/container.ts';
|
||||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||||
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage';
|
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage';
|
||||||
import merge from 'lodash-es/merge';
|
|
||||||
|
import { MediaError } from 'types/mediaError';
|
||||||
|
import { getMediaError } from 'utils/mediaError';
|
||||||
|
|
||||||
const UNLIMITED_ITEMS = -1;
|
const UNLIMITED_ITEMS = -1;
|
||||||
|
|
||||||
|
@ -125,7 +130,7 @@ function getItemsForPlayback(serverId, query) {
|
||||||
} else {
|
} else {
|
||||||
query.Limit = query.Limit || 300;
|
query.Limit = query.Limit || 300;
|
||||||
}
|
}
|
||||||
query.Fields = 'Chapters';
|
query.Fields = ['Chapters', 'Trickplay'];
|
||||||
query.ExcludeLocationTypes = 'Virtual';
|
query.ExcludeLocationTypes = 'Virtual';
|
||||||
query.EnableTotalRecordCount = false;
|
query.EnableTotalRecordCount = false;
|
||||||
query.CollapseBoxSetItems = false;
|
query.CollapseBoxSetItems = false;
|
||||||
|
@ -588,9 +593,18 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PlaybackManager} instance
|
||||||
|
* @param {import('@jellyfin/sdk/lib/generated-client/index.js').PlaybackInfoResponse} result
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function validatePlaybackInfoResult(instance, result) {
|
function validatePlaybackInfoResult(instance, result) {
|
||||||
if (result.ErrorCode) {
|
if (result.ErrorCode) {
|
||||||
showPlaybackInfoErrorMessage(instance, 'PlaybackError' + result.ErrorCode);
|
// NOTE: To avoid needing to retranslate the "NoCompatibleStream" message,
|
||||||
|
// we need to keep the key in the same format.
|
||||||
|
const errMessage = result.ErrorCode === PlaybackErrorCode.NoCompatibleStream ?
|
||||||
|
'PlaybackErrorNoCompatibleStream' : `PlaybackError.${result.ErrorCode}`;
|
||||||
|
showPlaybackInfoErrorMessage(instance, errMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1720,7 +1734,8 @@ class PlaybackManager {
|
||||||
streamInfo.resetSubtitleOffset = false;
|
streamInfo.resetSubtitleOffset = false;
|
||||||
|
|
||||||
if (!streamInfo.url) {
|
if (!streamInfo.url) {
|
||||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
cancelPlayback();
|
||||||
|
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1768,8 +1783,8 @@ class PlaybackManager {
|
||||||
playerData.isChangingStream = false;
|
playerData.isChangingStream = false;
|
||||||
|
|
||||||
onPlaybackError.call(player, e, {
|
onPlaybackError.call(player, e, {
|
||||||
type: 'mediadecodeerror',
|
type: getMediaError(e),
|
||||||
streamInfo: streamInfo
|
streamInfo
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1858,7 +1873,7 @@ class PlaybackManager {
|
||||||
IsVirtualUnaired: false,
|
IsVirtualUnaired: false,
|
||||||
IsMissing: false,
|
IsMissing: false,
|
||||||
UserId: apiClient.getCurrentUserId(),
|
UserId: apiClient.getCurrentUserId(),
|
||||||
Fields: 'Chapters'
|
Fields: ['Chapters', 'Trickplay']
|
||||||
}).then(function (episodesResult) {
|
}).then(function (episodesResult) {
|
||||||
const originalResults = episodesResult.Items;
|
const originalResults = episodesResult.Items;
|
||||||
const isSeries = firstItem.Type === 'Series';
|
const isSeries = firstItem.Type === 'Series';
|
||||||
|
@ -1940,7 +1955,7 @@ class PlaybackManager {
|
||||||
IsVirtualUnaired: false,
|
IsVirtualUnaired: false,
|
||||||
IsMissing: false,
|
IsMissing: false,
|
||||||
UserId: apiClient.getCurrentUserId(),
|
UserId: apiClient.getCurrentUserId(),
|
||||||
Fields: 'Chapters'
|
Fields: ['Chapters', 'Trickplay']
|
||||||
}).then(function (episodesResult) {
|
}).then(function (episodesResult) {
|
||||||
let foundItem = false;
|
let foundItem = false;
|
||||||
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
||||||
|
@ -2179,7 +2194,7 @@ class PlaybackManager {
|
||||||
|
|
||||||
// If it's still null then there's nothing to play
|
// If it's still null then there's nothing to play
|
||||||
if (!firstItem) {
|
if (!firstItem) {
|
||||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2551,8 +2566,8 @@ class PlaybackManager {
|
||||||
onPlaybackStarted(player, playOptions, streamInfo, mediaSource);
|
onPlaybackStarted(player, playOptions, streamInfo, mediaSource);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
onPlaybackError.call(player, err, {
|
onPlaybackError.call(player, err, {
|
||||||
type: 'mediadecodeerror',
|
type: getMediaError(err),
|
||||||
streamInfo: streamInfo
|
streamInfo
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
@ -2785,7 +2800,7 @@ class PlaybackManager {
|
||||||
return mediaSource;
|
return mediaSource;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -3194,22 +3209,32 @@ class PlaybackManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} streamInfo
|
||||||
|
* @param {MediaError} errorType
|
||||||
|
* @param {boolean} currentlyPreventsVideoStreamCopy
|
||||||
|
* @param {boolean} currentlyPreventsAudioStreamCopy
|
||||||
|
* @returns {boolean} Returns true if the stream should be retried by transcoding.
|
||||||
|
*/
|
||||||
function enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy) {
|
function enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy) {
|
||||||
// mediadecodeerror, medianotsupported, network, servererror
|
|
||||||
return streamInfo.mediaSource.SupportsTranscoding
|
return streamInfo.mediaSource.SupportsTranscoding
|
||||||
&& (!currentlyPreventsVideoStreamCopy || !currentlyPreventsAudioStreamCopy);
|
&& (!currentlyPreventsVideoStreamCopy || !currentlyPreventsAudioStreamCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playback error handler.
|
||||||
|
* @param {Error} e
|
||||||
|
* @param {object} error
|
||||||
|
* @param {object} error.streamInfo
|
||||||
|
* @param {MediaError} error.type
|
||||||
|
*/
|
||||||
function onPlaybackError(e, error) {
|
function onPlaybackError(e, error) {
|
||||||
const player = this;
|
const player = this;
|
||||||
error = error || {};
|
error = error || {};
|
||||||
|
|
||||||
// network
|
|
||||||
// mediadecodeerror
|
|
||||||
// medianotsupported
|
|
||||||
const errorType = error.type;
|
const errorType = error.type;
|
||||||
|
|
||||||
console.debug('playbackmanager playback error type: ' + (errorType || ''));
|
console.warn('[playbackmanager] onPlaybackError:', e, error);
|
||||||
|
|
||||||
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
|
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
|
||||||
|
|
||||||
|
@ -3235,8 +3260,7 @@ class PlaybackManager {
|
||||||
|
|
||||||
Events.trigger(self, 'playbackerror', [errorType]);
|
Events.trigger(self, 'playbackerror', [errorType]);
|
||||||
|
|
||||||
const displayErrorCode = 'NoCompatibleStream';
|
onPlaybackStopped.call(player, e, `.${errorType}`);
|
||||||
onPlaybackStopped.call(player, e, displayErrorCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlaybackStopped(e, displayErrorCode) {
|
function onPlaybackStopped(e, displayErrorCode) {
|
||||||
|
|
|
@ -179,6 +179,7 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
|
||||||
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
|
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
|
||||||
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
|
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
|
||||||
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
|
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
|
||||||
|
context.querySelector('.chkLimitSupportedVideoResolution').checked = appSettings.limitSupportedVideoResolution();
|
||||||
|
|
||||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||||
|
@ -194,8 +195,8 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
|
||||||
selectChromecastVersion.innerHTML = ccAppsHtml;
|
selectChromecastVersion.innerHTML = ccAppsHtml;
|
||||||
selectChromecastVersion.value = user.Configuration.CastReceiverId;
|
selectChromecastVersion.value = user.Configuration.CastReceiverId;
|
||||||
|
|
||||||
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
|
const selectMaxVideoWidth = context.querySelector('.selectMaxVideoWidth');
|
||||||
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
|
selectMaxVideoWidth.value = appSettings.maxVideoWidth();
|
||||||
|
|
||||||
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
|
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
|
||||||
fillSkipLengths(selectSkipForwardLength);
|
fillSkipLengths(selectSkipForwardLength);
|
||||||
|
@ -212,7 +213,8 @@ function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||||
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
|
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
|
||||||
|
|
||||||
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
|
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
|
||||||
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
|
appSettings.maxVideoWidth(context.querySelector('.selectMaxVideoWidth').value);
|
||||||
|
appSettings.limitSupportedVideoResolution(context.querySelector('.chkLimitSupportedVideoResolution').checked);
|
||||||
|
|
||||||
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||||
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||||
|
@ -309,7 +311,7 @@ function embed(options, self) {
|
||||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
|
options.element.querySelector('.selectMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
|
||||||
|
|
||||||
self.loadData();
|
self.loadData();
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selectContainer">
|
<div class="selectContainer">
|
||||||
<select is="emby-select" class="selectLabelMaxVideoWidth" label="${LabelMaxVideoResolution}">
|
<select is="emby-select" class="selectMaxVideoWidth" label="${LabelMaxVideoResolution}">
|
||||||
<option value="0">${Auto}</option>
|
<option value="0">${Auto}</option>
|
||||||
<option value="-1">${ScreenResolution}</option>
|
<option value="-1">${ScreenResolution}</option>
|
||||||
<option value="640">360p</option>
|
<option value="640">360p</option>
|
||||||
|
@ -54,6 +54,14 @@
|
||||||
<option value="7680">8K</option>
|
<option value="7680">8K</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkLimitSupportedVideoResolution" />
|
||||||
|
<span>${LimitSupportedVideoResolution}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${LimitSupportedVideoResolutionHelp}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="verticalSection verticalSection-extrabottompadding musicQualitySection hide">
|
<div class="verticalSection verticalSection-extrabottompadding musicQualitySection hide">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dom from '../scripts/dom';
|
import dom from '../scripts/dom';
|
||||||
import browser from '../scripts/browser';
|
import appSettings from 'scripts/settings/appSettings';
|
||||||
import layoutManager from './layoutManager';
|
import layoutManager from './layoutManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -477,7 +477,7 @@ function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
|
||||||
* Returns true if smooth scroll must be used.
|
* Returns true if smooth scroll must be used.
|
||||||
*/
|
*/
|
||||||
function useSmoothScroll() {
|
function useSmoothScroll() {
|
||||||
return !!browser.tizen;
|
return appSettings.enableSmoothScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -34,7 +34,7 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (api && user?.Id) {
|
if (api && user?.Id) {
|
||||||
getItemsApi(api)
|
getItemsApi(api)
|
||||||
.getItemsByUserId({
|
.getItems({
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
||||||
includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series, BaseItemKind.MusicArtist],
|
includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series, BaseItemKind.MusicArtist],
|
||||||
|
|
|
@ -26,8 +26,6 @@ import { getSystemInfoQuery } from 'hooks/useSystemInfo';
|
||||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
import { version as WEB_VERSION } from '../../../package.json';
|
|
||||||
|
|
||||||
import '../../elements/emby-button/emby-button';
|
import '../../elements/emby-button/emby-button';
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
@ -210,7 +208,7 @@ function refreshActiveRecordings(view, apiClient) {
|
||||||
|
|
||||||
function reloadSystemInfo(view, apiClient) {
|
function reloadSystemInfo(view, apiClient) {
|
||||||
view.querySelector('#buildVersion').innerText = __JF_BUILD_VERSION__;
|
view.querySelector('#buildVersion').innerText = __JF_BUILD_VERSION__;
|
||||||
view.querySelector('#webVersion').innerText = WEB_VERSION;
|
view.querySelector('#webVersion').innerText = __PACKAGE_JSON_VERSION__;
|
||||||
|
|
||||||
queryClient
|
queryClient
|
||||||
.fetchQuery(getSystemInfoQuery(toApi(apiClient)))
|
.fetchQuery(getSystemInfoQuery(toApi(apiClient)))
|
||||||
|
|
|
@ -47,15 +47,15 @@
|
||||||
<span>MPEG1</span>
|
<span>MPEG1</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg2video" data-types="amf,nvenc,qsv,vaapi,rkmpp,videotoolbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg2video" data-types="amf,nvenc,qsv,vaapi,rkmpp" />
|
||||||
<span>MPEG2</span>
|
<span>MPEG2</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="nvenc,rkmpp,videotoolbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="nvenc,rkmpp" />
|
||||||
<span>MPEG4</span>
|
<span>MPEG4</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vc1" data-types="amf,nvenc,qsv,vaapi,videotoolbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vc1" data-types="amf,nvenc,qsv,vaapi" />
|
||||||
<span>VC1</span>
|
<span>VC1</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
@ -123,6 +123,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkboxListContainer">
|
<div class="checkboxListContainer">
|
||||||
|
<h3 class="checkboxListLabel">${LabelEncodingFormatOptions}</h3>
|
||||||
|
<div class="fieldDescription">${EncodingFormatHelp}</div>
|
||||||
<div class="checkboxList">
|
<div class="checkboxList">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkAllowHevcEncoding" />
|
<input type="checkbox" is="emby-checkbox" id="chkAllowHevcEncoding" />
|
||||||
|
@ -135,6 +137,12 @@
|
||||||
<span>${AllowAv1Encoding}</span>
|
<span>${AllowAv1Encoding}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkboxList">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" id="chkAllowMjpegEncoding" />
|
||||||
|
<span>${AllowMjpegEncoding}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vppTonemappingOptions hide">
|
<div class="vppTonemappingOptions hide">
|
||||||
|
@ -155,6 +163,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="videoToolboxTonemappingOptions hide">
|
||||||
|
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" id="chkVideoToolboxTonemapping" />
|
||||||
|
<span>${EnableVideoToolboxTonemapping}</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription checkboxFieldDescription">${AllowVideoToolboxTonemappingHelp}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tonemappingOptions hide">
|
<div class="tonemappingOptions hide">
|
||||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||||
<label>
|
<label>
|
||||||
|
@ -235,9 +253,8 @@
|
||||||
<div class="inputContainer fldEncoderPath">
|
<div class="inputContainer fldEncoderPath">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<div style="flex-grow:1;">
|
<div style="flex-grow:1;">
|
||||||
<input is="emby-input" class="txtEncoderPath" label="${LabelffmpegPath}" autocomplete="off" dir="ltr" />
|
<input is="emby-input" class="txtEncoderPath" label="${LabelffmpegPath}" autocomplete="off" dir="ltr" disabled/>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" is="paper-icon-button-light" id="btnSelectEncoderPath" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
<div>${LabelffmpegPathHelp}</div>
|
<div>${LabelffmpegPathHelp}</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@ function loadPage(page, config, systemInfo) {
|
||||||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||||
page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding;
|
page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding;
|
||||||
|
page.querySelector('#chkAllowMjpegEncoding').checked = config.AllowMjpegEncoding;
|
||||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||||
$('#selectThreadCount', page).val(config.EncodingThreadCount);
|
$('#selectThreadCount', page).val(config.EncodingThreadCount);
|
||||||
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
|
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
|
||||||
|
@ -32,6 +33,7 @@ function loadPage(page, config, systemInfo) {
|
||||||
$('#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('#chkVideoToolboxTonemapping').checked = config.EnableVideoToolboxTonemapping;
|
||||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
|
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
|
||||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||||
|
@ -93,6 +95,7 @@ function onSubmit() {
|
||||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||||
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.EnableVideoToolboxTonemapping = form.querySelector('#chkVideoToolboxTonemapping').checked;
|
||||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||||
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
|
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
|
||||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||||
|
@ -125,6 +128,7 @@ function onSubmit() {
|
||||||
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||||
config.AllowAv1Encoding = form.querySelector('#chkAllowAv1Encoding').checked;
|
config.AllowAv1Encoding = form.querySelector('#chkAllowAv1Encoding').checked;
|
||||||
|
config.AllowMjpegEncoding = form.querySelector('#chkAllowMjpegEncoding').checked;
|
||||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||||
updateEncoder(form);
|
updateEncoder(form);
|
||||||
}, function () {
|
}, function () {
|
||||||
|
@ -175,6 +179,9 @@ function getTabs() {
|
||||||
}, {
|
}, {
|
||||||
href: '#/dashboard/playback/streaming',
|
href: '#/dashboard/playback/streaming',
|
||||||
name: globalize.translate('TabStreaming')
|
name: globalize.translate('TabStreaming')
|
||||||
|
}, {
|
||||||
|
href: '#/dashboard/playback/trickplay',
|
||||||
|
name: globalize.translate('Trickplay')
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +213,7 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp') {
|
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp' || this.value == 'videotoolbox') {
|
||||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||||
|
@ -218,6 +225,12 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||||
page.querySelector('.fldIntelLp').classList.add('hide');
|
page.querySelector('.fldIntelLp').classList.add('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.value === 'videotoolbox') {
|
||||||
|
page.querySelector('.videoToolboxTonemappingOptions').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
page.querySelector('.videoToolboxTonemappingOptions').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
||||||
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
|
@ -244,21 +257,6 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||||
|
|
||||||
setDecodingCodecsVisible(page, this.value);
|
setDecodingCodecsVisible(page, this.value);
|
||||||
});
|
});
|
||||||
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
|
||||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
|
||||||
const picker = new DirectoryBrowser();
|
|
||||||
picker.show({
|
|
||||||
includeFiles: true,
|
|
||||||
callback: function (path) {
|
|
||||||
if (path) {
|
|
||||||
$('.txtEncoderPath', page).val(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
picker.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
$('#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();
|
||||||
|
|
|
@ -39,6 +39,9 @@ function getTabs() {
|
||||||
}, {
|
}, {
|
||||||
href: '#/dashboard/playback/streaming',
|
href: '#/dashboard/playback/streaming',
|
||||||
name: globalize.translate('TabStreaming')
|
name: globalize.translate('TabStreaming')
|
||||||
|
}, {
|
||||||
|
href: '#/dashboard/playback/trickplay',
|
||||||
|
name: globalize.translate('Trickplay')
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,4 +55,3 @@ $(document).on('pageinit', '#playbackConfigurationPage', function () {
|
||||||
loadPage(page, config);
|
loadPage(page, config);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,14 @@ import markdownIt from 'markdown-it';
|
||||||
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';
|
||||||
import '../../../../elements/emby-button/emby-button';
|
|
||||||
import Dashboard from '../../../../utils/dashboard';
|
import Dashboard from '../../../../utils/dashboard';
|
||||||
import alert from '../../../../components/alert';
|
import alert from '../../../../components/alert';
|
||||||
import confirm from '../../../../components/confirm/confirm';
|
import confirm from '../../../../components/confirm/confirm';
|
||||||
|
|
||||||
|
import 'elements/emby-button/emby-button';
|
||||||
|
import 'elements/emby-collapse/emby-collapse';
|
||||||
|
import 'elements/emby-select/emby-select';
|
||||||
|
|
||||||
function populateHistory(packageInfo, page) {
|
function populateHistory(packageInfo, page) {
|
||||||
let html = '';
|
let html = '';
|
||||||
const length = Math.min(packageInfo.versions.length, 10);
|
const length = Math.min(packageInfo.versions.length, 10);
|
||||||
|
|
|
@ -30,6 +30,9 @@ function getTabs() {
|
||||||
}, {
|
}, {
|
||||||
href: '#/dashboard/playback/streaming',
|
href: '#/dashboard/playback/streaming',
|
||||||
name: globalize.translate('TabStreaming')
|
name: globalize.translate('TabStreaming')
|
||||||
|
}, {
|
||||||
|
href: '#/dashboard/playback/trickplay',
|
||||||
|
name: globalize.translate('Trickplay')
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ServerConnections from 'components/ServerConnections';
|
||||||
import dom from 'scripts/dom';
|
import dom from 'scripts/dom';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||||
|
|
||||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
import 'elements/emby-scroller/emby-scroller';
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
|
@ -133,7 +134,7 @@ function getFetchDataFn(section) {
|
||||||
return function () {
|
return function () {
|
||||||
const apiClient = this.apiClient;
|
const apiClient = this.apiClient;
|
||||||
const options = {
|
const options = {
|
||||||
SortBy: 'SeriesName,SortName',
|
SortBy: [ItemSortBy.SeriesSortName, ItemSortBy.SortName].join(','),
|
||||||
SortOrder: 'Ascending',
|
SortOrder: 'Ascending',
|
||||||
Filters: 'IsFavorite',
|
Filters: 'IsFavorite',
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import '../elements/emby-scroller/emby-scroller';
|
||||||
import ServerConnections from '../components/ServerConnections';
|
import ServerConnections from '../components/ServerConnections';
|
||||||
import LibraryMenu from '../scripts/libraryMenu';
|
import LibraryMenu from '../scripts/libraryMenu';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||||
|
|
||||||
function getInitialLiveTvQuery(instance, params, startIndex = 0, limit = 300) {
|
function getInitialLiveTvQuery(instance, params, startIndex = 0, limit = 300) {
|
||||||
const query = {
|
const query = {
|
||||||
|
@ -223,7 +224,7 @@ function updateAlphaPickerState(instance) {
|
||||||
if (alphaPicker) {
|
if (alphaPicker) {
|
||||||
const values = instance.getSortValues();
|
const values = instance.getSortValues();
|
||||||
|
|
||||||
if (values.sortBy.indexOf('SortName') !== -1) {
|
if (values.sortBy.indexOf(ItemSortBy.SortName) !== -1) {
|
||||||
alphaPicker.classList.remove('hide');
|
alphaPicker.classList.remove('hide');
|
||||||
instance.itemsContainer.parentNode.classList.add('padded-right-withalphapicker');
|
instance.itemsContainer.parentNode.classList.add('padded-right-withalphapicker');
|
||||||
} else {
|
} else {
|
||||||
|
@ -981,7 +982,7 @@ class ItemsView {
|
||||||
return sortNameOption.value;
|
return sortNameOption.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'IsFolder,' + sortNameOption.value;
|
return `${ItemSortBy.IsFolder},${sortNameOption.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortMenuOptions() {
|
getSortMenuOptions() {
|
||||||
|
@ -990,7 +991,7 @@ class ItemsView {
|
||||||
if (this.params.type === 'Programs') {
|
if (this.params.type === 'Programs') {
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('AirDate'),
|
name: globalize.translate('AirDate'),
|
||||||
value: 'StartDate,SortName'
|
value: [ItemSortBy.StartDate, ItemSortBy.SortName].join(',')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1015,7 +1016,7 @@ class ItemsView {
|
||||||
if (this.params.type !== 'Programs') {
|
if (this.params.type !== 'Programs') {
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('DateAdded'),
|
name: globalize.translate('DateAdded'),
|
||||||
value: 'DateCreated,SortName'
|
value: [ItemSortBy.DateCreated, ItemSortBy.SortName].join(',')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1029,13 +1030,13 @@ class ItemsView {
|
||||||
option = this.getNameSortOption(this.params);
|
option = this.getNameSortOption(this.params);
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('Folders'),
|
name: globalize.translate('Folders'),
|
||||||
value: 'IsFolder,' + option.value
|
value: `${ItemSortBy.IsFolder},${option.value}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('ParentalRating'),
|
name: globalize.translate('ParentalRating'),
|
||||||
value: 'OfficialRating,SortName'
|
value: [ItemSortBy.OfficialRating, ItemSortBy.SortName].join(',')
|
||||||
});
|
});
|
||||||
option = this.getPlayCountSortOption();
|
option = this.getPlayCountSortOption();
|
||||||
|
|
||||||
|
@ -1045,11 +1046,11 @@ class ItemsView {
|
||||||
|
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('ReleaseDate'),
|
name: globalize.translate('ReleaseDate'),
|
||||||
value: 'ProductionYear,PremiereDate,SortName'
|
value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName].join(',')
|
||||||
});
|
});
|
||||||
sortBy.push({
|
sortBy.push({
|
||||||
name: globalize.translate('Runtime'),
|
name: globalize.translate('Runtime'),
|
||||||
value: 'Runtime,SortName'
|
value: [ItemSortBy.Runtime, ItemSortBy.SortName].join(',')
|
||||||
});
|
});
|
||||||
return sortBy;
|
return sortBy;
|
||||||
}
|
}
|
||||||
|
@ -1058,13 +1059,13 @@ class ItemsView {
|
||||||
if (params.type === 'Episode') {
|
if (params.type === 'Episode') {
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('Name'),
|
name: globalize.translate('Name'),
|
||||||
value: 'SeriesName,SortName'
|
value: [ItemSortBy.SeriesSortName, ItemSortBy.SortName].join(',')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('Name'),
|
name: globalize.translate('Name'),
|
||||||
value: 'SortName'
|
value: ItemSortBy.SortName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1075,7 +1076,7 @@ class ItemsView {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('PlayCount'),
|
name: globalize.translate('PlayCount'),
|
||||||
value: 'PlayCount,SortName'
|
value: [ItemSortBy.PlayCount, ItemSortBy.SortName].join(',')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1086,7 +1087,7 @@ class ItemsView {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('DatePlayed'),
|
name: globalize.translate('DatePlayed'),
|
||||||
value: 'DatePlayed,SortName'
|
value: [ItemSortBy.DatePlayed, ItemSortBy.SortName].join(',')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1097,14 +1098,14 @@ class ItemsView {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('CriticRating'),
|
name: globalize.translate('CriticRating'),
|
||||||
value: 'CriticRating,SortName'
|
value: [ItemSortBy.CriticRating, ItemSortBy.SortName].join(',')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommunityRatingSortOption() {
|
getCommunityRatingSortOption() {
|
||||||
return {
|
return {
|
||||||
name: globalize.translate('CommunityRating'),
|
name: globalize.translate('CommunityRating'),
|
||||||
value: 'CommunityRating,SortName'
|
value: [ItemSortBy.CommunityRating, ItemSortBy.SortName].join(',')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,26 @@ export default function (view) {
|
||||||
btnUserRating.classList.add('hide');
|
btnUserRating.classList.add('hide');
|
||||||
btnUserRating.setItem(null);
|
btnUserRating.setItem(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update trickplay data
|
||||||
|
trickplayResolution = null;
|
||||||
|
|
||||||
|
const mediaSourceId = currentPlayer.streamInfo.mediaSource.Id;
|
||||||
|
const trickplayResolutions = item.Trickplay?.[mediaSourceId];
|
||||||
|
if (trickplayResolutions) {
|
||||||
|
// Prefer highest resolution <= 20% of total screen resolution width
|
||||||
|
let bestWidth;
|
||||||
|
const maxWidth = window.screen.width * window.devicePixelRatio * 0.2;
|
||||||
|
for (const [, info] of Object.entries(trickplayResolutions)) {
|
||||||
|
if (!bestWidth
|
||||||
|
|| (info.Width < bestWidth && bestWidth > maxWidth) // Objects not guaranteed to be sorted in any order, first width might be > maxWidth.
|
||||||
|
|| (info.Width > bestWidth && info.Width <= maxWidth)) {
|
||||||
|
bestWidth = info.Width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestWidth) trickplayResolution = trickplayResolutions[bestWidth];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayTimeWithoutAmPm(date, showSeconds) {
|
function getDisplayTimeWithoutAmPm(date, showSeconds) {
|
||||||
|
@ -1356,6 +1376,81 @@ export default function (view) {
|
||||||
resetIdle();
|
resetIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTrickplayBubbleHtml(apiClient, trickplayInfo, item, mediaSourceId, bubble, positionTicks) {
|
||||||
|
let doFullUpdate = false;
|
||||||
|
let chapterThumbContainer = bubble.querySelector('.chapterThumbContainer');
|
||||||
|
let chapterThumb;
|
||||||
|
let chapterThumbText;
|
||||||
|
|
||||||
|
// Create bubble elements if they don't already exist
|
||||||
|
if (chapterThumbContainer) {
|
||||||
|
chapterThumb = chapterThumbContainer.querySelector('.chapterThumb');
|
||||||
|
chapterThumbText = chapterThumbContainer.querySelector('.chapterThumbText');
|
||||||
|
} else {
|
||||||
|
doFullUpdate = true;
|
||||||
|
|
||||||
|
chapterThumbContainer = document.createElement('div');
|
||||||
|
chapterThumbContainer.classList.add('chapterThumbContainer');
|
||||||
|
chapterThumbContainer.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const chapterThumbWrapper = document.createElement('div');
|
||||||
|
chapterThumbWrapper.classList.add('chapterThumbWrapper');
|
||||||
|
chapterThumbWrapper.style.overflow = 'hidden';
|
||||||
|
chapterThumbWrapper.style.position = 'relative';
|
||||||
|
chapterThumbWrapper.style.width = trickplayInfo.Width + 'px';
|
||||||
|
chapterThumbWrapper.style.height = trickplayInfo.Height + 'px';
|
||||||
|
chapterThumbContainer.appendChild(chapterThumbWrapper);
|
||||||
|
|
||||||
|
chapterThumb = document.createElement('img');
|
||||||
|
chapterThumb.classList.add('chapterThumb');
|
||||||
|
chapterThumb.style.position = 'absolute';
|
||||||
|
chapterThumb.style.width = 'unset';
|
||||||
|
chapterThumb.style.minWidth = 'unset';
|
||||||
|
chapterThumb.style.height = 'unset';
|
||||||
|
chapterThumb.style.minHeight = 'unset';
|
||||||
|
chapterThumbWrapper.appendChild(chapterThumb);
|
||||||
|
|
||||||
|
const chapterThumbTextContainer = document.createElement('div');
|
||||||
|
chapterThumbTextContainer.classList.add('chapterThumbTextContainer');
|
||||||
|
chapterThumbContainer.appendChild(chapterThumbTextContainer);
|
||||||
|
|
||||||
|
chapterThumbText = document.createElement('h2');
|
||||||
|
chapterThumbText.classList.add('chapterThumbText');
|
||||||
|
chapterThumbTextContainer.appendChild(chapterThumbText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update trickplay values
|
||||||
|
const currentTimeMs = positionTicks / 10_000;
|
||||||
|
const currentTile = Math.floor(currentTimeMs / trickplayInfo.Interval);
|
||||||
|
|
||||||
|
const tileSize = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
|
||||||
|
const tileOffset = currentTile % tileSize;
|
||||||
|
const index = Math.floor(currentTile / tileSize);
|
||||||
|
|
||||||
|
const tileOffsetX = tileOffset % trickplayInfo.TileWidth;
|
||||||
|
const tileOffsetY = Math.floor(tileOffset / trickplayInfo.TileWidth);
|
||||||
|
const offsetX = -(tileOffsetX * trickplayInfo.Width);
|
||||||
|
const offsetY = -(tileOffsetY * trickplayInfo.Height);
|
||||||
|
|
||||||
|
const imgSrc = apiClient.getUrl('Videos/' + item.Id + '/Trickplay/' + trickplayInfo.Width + '/' + index + '.jpg', {
|
||||||
|
api_key: apiClient.accessToken(),
|
||||||
|
MediaSourceId: mediaSourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chapterThumb.src != imgSrc) chapterThumb.src = imgSrc;
|
||||||
|
chapterThumb.style.left = offsetX + 'px';
|
||||||
|
chapterThumb.style.top = offsetY + 'px';
|
||||||
|
|
||||||
|
chapterThumbText.textContent = datetime.getDisplayRunningTime(positionTicks);
|
||||||
|
|
||||||
|
// Set bubble innerHTML if container isn't part of DOM
|
||||||
|
if (doFullUpdate) {
|
||||||
|
bubble.innerHTML = chapterThumbContainer.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function getImgUrl(item, chapter, index, maxWidth, apiClient) {
|
function getImgUrl(item, chapter, index, maxWidth, apiClient) {
|
||||||
if (chapter.ImageTag) {
|
if (chapter.ImageTag) {
|
||||||
return apiClient.getScaledImageUrl(item.Id, {
|
return apiClient.getScaledImageUrl(item.Id, {
|
||||||
|
@ -1455,6 +1550,7 @@ export default function (view) {
|
||||||
let programEndDateMs = 0;
|
let programEndDateMs = 0;
|
||||||
let playbackStartTimeTicks = 0;
|
let playbackStartTimeTicks = 0;
|
||||||
let subtitleSyncOverlay;
|
let subtitleSyncOverlay;
|
||||||
|
let trickplayResolution = null;
|
||||||
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
|
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
|
||||||
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
|
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
|
||||||
const nowPlayingPositionSlider = view.querySelector('.osdPositionSlider');
|
const nowPlayingPositionSlider = view.querySelector('.osdPositionSlider');
|
||||||
|
@ -1681,6 +1777,25 @@ export default function (view) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
nowPlayingPositionSlider.updateBubbleHtml = function(bubble, value) {
|
||||||
|
showOsd();
|
||||||
|
|
||||||
|
const item = currentItem;
|
||||||
|
const ticks = currentRuntimeTicks * value / 100;
|
||||||
|
|
||||||
|
if (trickplayResolution && item?.Trickplay) {
|
||||||
|
return updateTrickplayBubbleHtml(
|
||||||
|
ServerConnections.getApiClient(item.ServerId),
|
||||||
|
trickplayResolution,
|
||||||
|
item,
|
||||||
|
currentPlayer.streamInfo.mediaSource.Id,
|
||||||
|
bubble,
|
||||||
|
ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
nowPlayingPositionSlider.getBubbleHtml = function (value) {
|
nowPlayingPositionSlider.getBubbleHtml = function (value) {
|
||||||
showOsd();
|
showOsd();
|
||||||
if (enableProgressByTimeOfDay) {
|
if (enableProgressByTimeOfDay) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { LibraryTab } from 'types/libraryTab';
|
||||||
import { getBackdropShape } from 'utils/card';
|
import { getBackdropShape } from 'utils/card';
|
||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
import Events from 'utils/events';
|
import Events from 'utils/events';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
|
||||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
import 'elements/emby-button/emby-button';
|
import 'elements/emby-button/emby-button';
|
||||||
|
@ -332,7 +333,7 @@ export default function (view, params) {
|
||||||
function onInputCommand(e) {
|
function onInputCommand(e) {
|
||||||
if (e.detail.command === 'search') {
|
if (e.detail.command === 'search') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Dashboard.navigate('search.html?collectionType=tv&parentId=' + params.topParentId);
|
Dashboard.navigate(`search.html?collectionType=${CollectionType.Tvshows}&parentId=${params.topParentId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,13 @@
|
||||||
<div class="fieldDescription checkboxFieldDescription">${EnableGamepadHelp}</div>
|
<div class="fieldDescription checkboxFieldDescription">${EnableGamepadHelp}</div>
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LabelPleaseRestart}</div>
|
<div class="fieldDescription checkboxFieldDescription">${LabelPleaseRestart}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription smoothScrollContainer hide">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" class="chkSmoothScroll" />
|
||||||
|
<span>${EnableSmoothScroll}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block btnSave hide">
|
<button is="emby-button" type="submit" class="raised button-submit block btnSave hide">
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import layoutManager from 'components/layoutManager';
|
||||||
import toast from '../../../components/toast/toast';
|
import toast from '../../../components/toast/toast';
|
||||||
import globalize from '../../../scripts/globalize';
|
import globalize from '../../../scripts/globalize';
|
||||||
import appSettings from '../../../scripts/settings/appSettings';
|
import appSettings from '../../../scripts/settings/appSettings';
|
||||||
|
@ -6,6 +7,7 @@ import Events from '../../../utils/events.ts';
|
||||||
export default function (view) {
|
export default function (view) {
|
||||||
function submit(e) {
|
function submit(e) {
|
||||||
appSettings.enableGamepad(view.querySelector('.chkEnableGamepad').checked);
|
appSettings.enableGamepad(view.querySelector('.chkEnableGamepad').checked);
|
||||||
|
appSettings.enableSmoothScroll(view.querySelector('.chkSmoothScroll').checked);
|
||||||
|
|
||||||
toast(globalize.translate('SettingsSaved'));
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
|
||||||
|
@ -17,7 +19,11 @@ export default function (view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
view.addEventListener('viewshow', function () {
|
view.addEventListener('viewshow', function () {
|
||||||
|
view.querySelector('.smoothScrollContainer').classList.toggle('hide', !layoutManager.tv);
|
||||||
|
|
||||||
view.querySelector('.chkEnableGamepad').checked = appSettings.enableGamepad();
|
view.querySelector('.chkEnableGamepad').checked = appSettings.enableGamepad();
|
||||||
|
view.querySelector('.chkSmoothScroll').checked = appSettings.enableSmoothScroll();
|
||||||
|
|
||||||
view.querySelector('form').addEventListener('submit', submit);
|
view.querySelector('form').addEventListener('submit', submit);
|
||||||
view.querySelector('.btnSave').classList.remove('hide');
|
view.querySelector('.btnSave').classList.remove('hide');
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,10 @@ function updateBubble(range, percent, value, bubble) {
|
||||||
|
|
||||||
let html;
|
let html;
|
||||||
|
|
||||||
|
if (range.updateBubbleHtml?.(bubble, value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (range.getBubbleHtml) {
|
if (range.getBubbleHtml) {
|
||||||
html = range.getBubbleHtml(percent, value);
|
html = range.getBubbleHtml(percent, value);
|
||||||
} else {
|
} else {
|
||||||
|
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
|
@ -16,6 +16,8 @@ export declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
const __JF_BUILD_VERSION__: string;
|
const __JF_BUILD_VERSION__: string;
|
||||||
|
const __PACKAGE_JSON_NAME__: string;
|
||||||
|
const __PACKAGE_JSON_VERSION__: string;
|
||||||
const __USE_SYSTEM_FONTS__: string;
|
const __USE_SYSTEM_FONTS__: string;
|
||||||
const __WEBPACK_SERVE__: string;
|
const __WEBPACK_SERVE__: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,9 @@ function loadCoreDictionary() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
// Log current version to console to help out with issue triage and debugging
|
||||||
|
console.log(`${__PACKAGE_JSON_NAME__} version ${__PACKAGE_JSON_VERSION__} build ${__JF_BUILD_VERSION__}`);
|
||||||
|
|
||||||
// This is used in plugins
|
// This is used in plugins
|
||||||
window.Events = Events;
|
window.Events = Events;
|
||||||
window.TaskButton = taskButton;
|
window.TaskButton = taskButton;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import profileBuilder from '../../scripts/browserDeviceProfile';
|
||||||
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
|
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
|
||||||
import { PluginType } from '../../types/plugin.ts';
|
import { PluginType } from '../../types/plugin.ts';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
|
import { MediaError } from 'types/mediaError';
|
||||||
|
|
||||||
function getDefaultProfile() {
|
function getDefaultProfile() {
|
||||||
return profileBuilder({});
|
return profileBuilder({});
|
||||||
|
@ -343,7 +344,7 @@ class HtmlAudioPlayer {
|
||||||
return;
|
return;
|
||||||
case 2:
|
case 2:
|
||||||
// MEDIA_ERR_NETWORK
|
// MEDIA_ERR_NETWORK
|
||||||
type = 'network';
|
type = MediaError.NETWORK_ERROR;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// MEDIA_ERR_DECODE
|
// MEDIA_ERR_DECODE
|
||||||
|
@ -351,12 +352,12 @@ class HtmlAudioPlayer {
|
||||||
htmlMediaHelper.handleHlsJsMediaError(self);
|
htmlMediaHelper.handleHlsJsMediaError(self);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
type = 'mediadecodeerror';
|
type = MediaError.MEDIA_DECODE_ERROR;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||||
type = 'medianotsupported';
|
type = MediaError.MEDIA_NOT_SUPPORTED;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// seeing cases where Edge is firing error events with no error code
|
// seeing cases where Edge is firing error events with no error code
|
||||||
|
|
|
@ -37,6 +37,7 @@ import Events from '../../utils/events.ts';
|
||||||
import { includesAny } from '../../utils/container.ts';
|
import { includesAny } from '../../utils/container.ts';
|
||||||
import { isHls } from '../../utils/mediaSource.ts';
|
import { isHls } from '../../utils/mediaSource.ts';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import { MediaError } from 'types/mediaError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns resolved URL.
|
* Returns resolved URL.
|
||||||
|
@ -217,7 +218,7 @@ export class HtmlVideoPlayer {
|
||||||
*/
|
*/
|
||||||
#currentAssRenderer;
|
#currentAssRenderer;
|
||||||
/**
|
/**
|
||||||
* @type {null | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#customTrackIndex;
|
#customTrackIndex;
|
||||||
/**
|
/**
|
||||||
|
@ -443,6 +444,7 @@ export class HtmlVideoPlayer {
|
||||||
startPosition: options.playerStartPositionTicks / 10000000,
|
startPosition: options.playerStartPositionTicks / 10000000,
|
||||||
manifestLoadingTimeOut: 20000,
|
manifestLoadingTimeOut: 20000,
|
||||||
maxBufferLength: maxBufferLength,
|
maxBufferLength: maxBufferLength,
|
||||||
|
videoPreference: { preferHDR: true },
|
||||||
xhrSetup(xhr) {
|
xhrSetup(xhr) {
|
||||||
xhr.withCredentials = includeCorsCredentials;
|
xhr.withCredentials = includeCorsCredentials;
|
||||||
}
|
}
|
||||||
|
@ -519,7 +521,7 @@ export class HtmlVideoPlayer {
|
||||||
|
|
||||||
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && isHls(options.mediaSource)) {
|
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && isHls(options.mediaSource)) {
|
||||||
return this.setSrcWithHlsJs(elem, options, val);
|
return this.setSrcWithHlsJs(elem, options, val);
|
||||||
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') {
|
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container?.toUpperCase() === 'FLV') {
|
||||||
return this.setSrcWithFlvJs(elem, options, val);
|
return this.setSrcWithFlvJs(elem, options, val);
|
||||||
} else {
|
} else {
|
||||||
elem.autoplay = true;
|
elem.autoplay = true;
|
||||||
|
@ -982,6 +984,8 @@ export class HtmlVideoPlayer {
|
||||||
seekOnPlaybackStart(this, e.target, this._currentPlayOptions.playerStartPositionTicks, () => {
|
seekOnPlaybackStart(this, e.target, this._currentPlayOptions.playerStartPositionTicks, () => {
|
||||||
if (this.#currentAssRenderer) {
|
if (this.#currentAssRenderer) {
|
||||||
this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset;
|
this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset;
|
||||||
|
this.#currentAssRenderer.resize();
|
||||||
|
this.#currentAssRenderer.resetRenderAheadCache(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1018,7 +1022,7 @@ export class HtmlVideoPlayer {
|
||||||
// Only trigger this if there is media info
|
// Only trigger this if there is media info
|
||||||
// Avoid triggering in situations where it might not actually have a video stream (audio only live tv channel)
|
// Avoid triggering in situations where it might not actually have a video stream (audio only live tv channel)
|
||||||
if (!mediaSource || mediaSource.RunTimeTicks) {
|
if (!mediaSource || mediaSource.RunTimeTicks) {
|
||||||
onErrorInternal(this, 'mediadecodeerror');
|
onErrorInternal(this, MediaError.NO_MEDIA_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1070,7 +1074,7 @@ export class HtmlVideoPlayer {
|
||||||
return;
|
return;
|
||||||
case 2:
|
case 2:
|
||||||
// MEDIA_ERR_NETWORK
|
// MEDIA_ERR_NETWORK
|
||||||
type = 'network';
|
type = MediaError.NETWORK_ERROR;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// MEDIA_ERR_DECODE
|
// MEDIA_ERR_DECODE
|
||||||
|
@ -1078,12 +1082,12 @@ export class HtmlVideoPlayer {
|
||||||
handleHlsJsMediaError(this);
|
handleHlsJsMediaError(this);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
type = 'mediadecodeerror';
|
type = MediaError.MEDIA_DECODE_ERROR;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||||
type = 'medianotsupported';
|
type = MediaError.MEDIA_NOT_SUPPORTED;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// seeing cases where Edge is firing error events with no error code
|
// seeing cases where Edge is firing error events with no error code
|
||||||
|
@ -1168,9 +1172,9 @@ export class HtmlVideoPlayer {
|
||||||
this.#currentClock = null;
|
this.#currentClock = null;
|
||||||
this._currentAspectRatio = null;
|
this._currentAspectRatio = null;
|
||||||
|
|
||||||
const jassub = this.#currentAssRenderer;
|
const octopus = this.#currentAssRenderer;
|
||||||
if (jassub) {
|
if (octopus) {
|
||||||
jassub.destroy();
|
octopus.dispose();
|
||||||
}
|
}
|
||||||
this.#currentAssRenderer = null;
|
this.#currentAssRenderer = null;
|
||||||
}
|
}
|
||||||
|
@ -1259,43 +1263,36 @@ export class HtmlVideoPlayer {
|
||||||
const fallbackFontList = apiClient.getUrl('/FallbackFont/Fonts', {
|
const fallbackFontList = apiClient.getUrl('/FallbackFont/Fonts', {
|
||||||
api_key: apiClient.accessToken()
|
api_key: apiClient.accessToken()
|
||||||
});
|
});
|
||||||
// TODO: replace with `event-target-polyfill` once https://github.com/benlesh/event-target-polyfill/pull/12 or 11 is merged
|
const htmlVideoPlayer = this;
|
||||||
import('event-target-polyfill').then(() => {
|
import('@jellyfin/libass-wasm').then(({ default: SubtitlesOctopus }) => {
|
||||||
import('jassub').then(({ default: JASSUB }) => {
|
|
||||||
// test SIMD support
|
|
||||||
JASSUB._test();
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
video: videoElement,
|
video: videoElement,
|
||||||
subUrl: getTextTrackUrl(track, item),
|
subUrl: getTextTrackUrl(track, item),
|
||||||
fonts: avaliableFonts,
|
fonts: avaliableFonts,
|
||||||
fallbackFont: 'liberation sans',
|
workerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker.js`,
|
||||||
availableFonts: { 'liberation sans': `${appRouter.baseUrl()}/default.woff2` },
|
legacyWorkerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker-legacy.js`,
|
||||||
// Disabled eslint compat, but is safe as corejs3 polyfills URL
|
onError() {
|
||||||
// eslint-disable-next-line compat/compat
|
// HACK: Clear JavascriptSubtitlesOctopus: it gets disposed when an error occurs
|
||||||
workerUrl: new URL('jassub/dist/jassub-worker.js', import.meta.url).href,
|
htmlVideoPlayer.#currentAssRenderer = null;
|
||||||
// eslint-disable-next-line compat/compat
|
|
||||||
wasmUrl: new URL('jassub/dist/jassub-worker.wasm', import.meta.url).href,
|
// HACK: Give JavascriptSubtitlesOctopus time to dispose itself
|
||||||
// eslint-disable-next-line compat/compat
|
setTimeout(() => {
|
||||||
legacyWasmUrl: new URL('jassub/dist/jassub-worker.wasm.js', import.meta.url).href,
|
onErrorInternal(this, MediaError.ASS_RENDER_ERROR);
|
||||||
// eslint-disable-next-line compat/compat
|
}, 0);
|
||||||
modernWasmUrl : new URL('jassub/dist/jassub-worker-modern.wasm', import.meta.url).href,
|
},
|
||||||
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000,
|
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000,
|
||||||
// new jassub options; override all, even defaults
|
|
||||||
blendMode: 'js',
|
// new octopus options; override all, even defaults
|
||||||
asyncRender: true,
|
renderMode: 'wasm-blend',
|
||||||
offscreenRender: true,
|
|
||||||
// RVFC is polyfilled everywhere, but webOS 2 reports polyfill API's as functional even tho they aren't
|
|
||||||
onDemandRender: browser.web0sVersion !== 2,
|
|
||||||
useLocalFonts: true,
|
|
||||||
dropAllAnimations: false,
|
dropAllAnimations: false,
|
||||||
dropAllBlur: !JASSUB._supportsSIMD,
|
|
||||||
libassMemoryLimit: 40,
|
libassMemoryLimit: 40,
|
||||||
libassGlyphLimit: 40,
|
libassGlyphLimit: 40,
|
||||||
targetFps: 24,
|
targetFps: 24,
|
||||||
prescaleFactor: 0.8,
|
prescaleFactor: 0.8,
|
||||||
prescaleHeightLimit: 1080,
|
prescaleHeightLimit: 1080,
|
||||||
maxRenderHeight: 2160
|
maxRenderHeight: 2160,
|
||||||
|
resizeVariation: 0.2,
|
||||||
|
renderAhead: 90
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
@ -1307,12 +1304,6 @@ export class HtmlVideoPlayer {
|
||||||
options.workerUrl = workerUrl;
|
options.workerUrl = workerUrl;
|
||||||
options.legacyWorkerUrl = legacyWorkerUrl;
|
options.legacyWorkerUrl = legacyWorkerUrl;
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
this.#currentAssRenderer.destroy();
|
|
||||||
this.#currentAssRenderer = null;
|
|
||||||
onErrorInternal(this, 'mediadecodeerror');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.EnableFallbackFont) {
|
if (config.EnableFallbackFont) {
|
||||||
apiClient.getJSON(fallbackFontList).then((fontFiles = []) => {
|
apiClient.getJSON(fallbackFontList).then((fontFiles = []) => {
|
||||||
fontFiles.forEach(font => {
|
fontFiles.forEach(font => {
|
||||||
|
@ -1321,16 +1312,13 @@ export class HtmlVideoPlayer {
|
||||||
});
|
});
|
||||||
avaliableFonts.push(fontUrl);
|
avaliableFonts.push(fontUrl);
|
||||||
});
|
});
|
||||||
this.#currentAssRenderer = new JASSUB(options);
|
this.#currentAssRenderer = new SubtitlesOctopus(options);
|
||||||
this.#currentAssRenderer.addEventListener('error', cleanup, { once: true });
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.#currentAssRenderer = new JASSUB(options);
|
this.#currentAssRenderer = new SubtitlesOctopus(options);
|
||||||
this.#currentAssRenderer.addEventListener('error', cleanup, { once: true });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoPlayerContainer .JASSUB {
|
.videoPlayerContainer .libassjs-canvas-parent {
|
||||||
order: -1;
|
order: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ export function getItemsForPlayback(apiClient, query) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
query.Limit = query.Limit || 300;
|
query.Limit = query.Limit || 300;
|
||||||
query.Fields = 'Chapters';
|
query.Fields = ['Chapters', 'Trickplay'];
|
||||||
query.ExcludeLocationTypes = 'Virtual';
|
query.ExcludeLocationTypes = 'Virtual';
|
||||||
query.EnableTotalRecordCount = false;
|
query.EnableTotalRecordCount = false;
|
||||||
query.CollapseBoxSetItems = false;
|
query.CollapseBoxSetItems = false;
|
||||||
|
@ -200,7 +200,7 @@ export function translateItemsForPlayback(apiClient, items, options) {
|
||||||
IsVirtualUnaired: false,
|
IsVirtualUnaired: false,
|
||||||
IsMissing: false,
|
IsMissing: false,
|
||||||
UserId: apiClient.getCurrentUserId(),
|
UserId: apiClient.getCurrentUserId(),
|
||||||
Fields: 'Chapters'
|
Fields: ['Chapters', 'Trickplay']
|
||||||
}).then(function (episodesResult) {
|
}).then(function (episodesResult) {
|
||||||
let foundItem = false;
|
let foundItem = false;
|
||||||
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import browser from './browser';
|
||||||
import appSettings from './settings/appSettings';
|
import appSettings from './settings/appSettings';
|
||||||
import * as userSettings from './settings/userSettings';
|
import * as userSettings from './settings/userSettings';
|
||||||
import browser from './browser';
|
|
||||||
|
|
||||||
function canPlayH264(videoTestElement) {
|
function canPlayH264(videoTestElement) {
|
||||||
return !!(videoTestElement.canPlayType?.('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
|
return !!(videoTestElement.canPlayType?.('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
|
||||||
|
@ -68,7 +68,7 @@ function canPlayNativeHls() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPlayNativeHlsInFmp4() {
|
function canPlayNativeHlsInFmp4() {
|
||||||
if (browser.tizenVersion >= 3 || browser.web0sVersion >= 3.5) {
|
if (browser.tizenVersion >= 5 || browser.web0sVersion >= 3.5) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,12 +210,22 @@ function supportsDolbyVision(options) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPlayDolbyVisionHevc(videoTestElement) {
|
function supportedDolbyVisionProfilesHevc(videoTestElement) {
|
||||||
// Profiles 5/7/8 4k@60fps
|
const supportedProfiles = [];
|
||||||
return !!videoTestElement.canPlayType
|
// Profiles 5/8 4k@60fps
|
||||||
&& (videoTestElement.canPlayType('video/mp4; codecs="dvh1.05.09"').replace(/no/, '')
|
if (videoTestElement.canPlayType) {
|
||||||
&& videoTestElement.canPlayType('video/mp4; codecs="dvh1.07.09"').replace(/no/, '')
|
if (videoTestElement
|
||||||
&& videoTestElement.canPlayType('video/mp4; codecs="dvh1.08.09"').replace(/no/, ''));
|
.canPlayType('video/mp4; codecs="dvh1.05.09"')
|
||||||
|
.replace(/no/, '')) {
|
||||||
|
supportedProfiles.push(5);
|
||||||
|
}
|
||||||
|
if (videoTestElement
|
||||||
|
.canPlayType('video/mp4; codecs="dvh1.08.09"')
|
||||||
|
.replace(/no/, '')) {
|
||||||
|
supportedProfiles.push(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return supportedProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirectPlayProfileForVideoContainer(container, videoAudioCodecs, videoTestElement, options) {
|
function getDirectPlayProfileForVideoContainer(container, videoAudioCodecs, videoTestElement, options) {
|
||||||
|
@ -942,9 +952,15 @@ export default function (options) {
|
||||||
av1VideoRangeTypes += '|HLG';
|
av1VideoRangeTypes += '|HLG';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportsDolbyVision(options) && canPlayDolbyVisionHevc(videoTestElement)) {
|
if (supportsDolbyVision(options)) {
|
||||||
|
const profiles = supportedDolbyVisionProfilesHevc(videoTestElement);
|
||||||
|
if (profiles.includes(5)) {
|
||||||
hevcVideoRangeTypes += '|DOVI';
|
hevcVideoRangeTypes += '|DOVI';
|
||||||
}
|
}
|
||||||
|
if (profiles.includes(8)) {
|
||||||
|
hevcVideoRangeTypes += '|DOVIWithHDR10|DOVIWithHLG|DOVIWithSDR';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const h264CodecProfileConditions = [
|
const h264CodecProfileConditions = [
|
||||||
{
|
{
|
||||||
|
@ -1131,7 +1147,7 @@ export default function (options) {
|
||||||
|
|
||||||
// On iOS 12.x, for TS container max h264 level is 4.2
|
// On iOS 12.x, for TS container max h264 level is 4.2
|
||||||
if (browser.iOS && browser.iOSVersion < 13) {
|
if (browser.iOS && browser.iOSVersion < 13) {
|
||||||
const codecProfile = {
|
const codecProfileTS = {
|
||||||
Type: 'Video',
|
Type: 'Video',
|
||||||
Codec: 'h264',
|
Codec: 'h264',
|
||||||
Container: 'ts',
|
Container: 'ts',
|
||||||
|
@ -1140,14 +1156,32 @@ export default function (options) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
codecProfile.Conditions.push({
|
codecProfileTS.Conditions.push({
|
||||||
Condition: 'LessThanEqual',
|
Condition: 'LessThanEqual',
|
||||||
Property: 'VideoLevel',
|
Property: 'VideoLevel',
|
||||||
Value: '42',
|
Value: '42',
|
||||||
IsRequired: false
|
IsRequired: false
|
||||||
});
|
});
|
||||||
|
|
||||||
profile.CodecProfiles.push(codecProfile);
|
profile.CodecProfiles.push(codecProfileTS);
|
||||||
|
|
||||||
|
const codecProfileMp4 = {
|
||||||
|
Type: 'Video',
|
||||||
|
Codec: 'h264',
|
||||||
|
Container: 'mp4',
|
||||||
|
Conditions: h264CodecProfileConditions.filter((condition) => {
|
||||||
|
return condition.Property !== 'VideoLevel';
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
codecProfileMp4.Conditions.push({
|
||||||
|
Condition: 'LessThanEqual',
|
||||||
|
Property: 'VideoLevel',
|
||||||
|
Value: '42',
|
||||||
|
IsRequired: false
|
||||||
|
});
|
||||||
|
|
||||||
|
profile.CodecProfiles.push(codecProfileMp4);
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.CodecProfiles.push({
|
profile.CodecProfiles.push({
|
||||||
|
@ -1156,6 +1190,24 @@ export default function (options) {
|
||||||
Conditions: h264CodecProfileConditions
|
Conditions: h264CodecProfileConditions
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (browser.web0s && supportsDolbyVision(options)) {
|
||||||
|
// Disallow direct playing of DOVI media in containers not mp4.
|
||||||
|
// This paired with the "Prefer fMP4-HLS Container" client playback setting enables DOVI playback on webOS.
|
||||||
|
profile.CodecProfiles.push({
|
||||||
|
Type: 'Video',
|
||||||
|
Container: '-mp4',
|
||||||
|
Codec: 'hevc',
|
||||||
|
Conditions: [
|
||||||
|
{
|
||||||
|
Condition: 'EqualsAny',
|
||||||
|
Property: 'VideoRangeType',
|
||||||
|
Value: 'SDR|HDR10|HLG',
|
||||||
|
IsRequired: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
profile.CodecProfiles.push({
|
profile.CodecProfiles.push({
|
||||||
Type: 'Video',
|
Type: 'Video',
|
||||||
Codec: 'hevc',
|
Codec: 'hevc',
|
||||||
|
@ -1232,4 +1284,3 @@ export default function (options) {
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,25 +4,38 @@ import { appRouter } from '../components/router/appRouter';
|
||||||
import globalize from './globalize';
|
import globalize from './globalize';
|
||||||
import ServerConnections from '../components/ServerConnections';
|
import ServerConnections from '../components/ServerConnections';
|
||||||
import alert from '../components/alert';
|
import alert from '../components/alert';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
|
||||||
function alertText(options) {
|
function alertText(options) {
|
||||||
return alert(options);
|
return alert(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDeletionConfirmContent(item) {
|
||||||
|
if (item.Type === BaseItemKind.Series) {
|
||||||
|
const totalEpisodes = item.RecursiveItemCount;
|
||||||
|
return {
|
||||||
|
title: globalize.translate('HeaderDeleteSeries'),
|
||||||
|
text: globalize.translate('ConfirmDeleteSeries', totalEpisodes),
|
||||||
|
confirmText: globalize.translate('DeleteEntireSeries', totalEpisodes),
|
||||||
|
primary: 'delete'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: globalize.translate('HeaderDeleteItem'),
|
||||||
|
text: globalize.translate('ConfirmDeleteItem'),
|
||||||
|
confirmText: globalize.translate('Delete'),
|
||||||
|
primary: 'delete'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteItem(options) {
|
export function deleteItem(options) {
|
||||||
const item = options.item;
|
const item = options.item;
|
||||||
const parentId = item.SeasonId || item.SeriesId || item.ParentId;
|
const parentId = item.SeasonId || item.SeriesId || item.ParentId;
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||||
|
|
||||||
return confirm({
|
return confirm(getDeletionConfirmContent(item)).then(function () {
|
||||||
|
|
||||||
title: globalize.translate('HeaderDeleteItem'),
|
|
||||||
text: globalize.translate('ConfirmDeleteItem'),
|
|
||||||
confirmText: globalize.translate('Delete'),
|
|
||||||
primary: 'delete'
|
|
||||||
|
|
||||||
}).then(function () {
|
|
||||||
return apiClient.deleteItem(item.Id).then(function () {
|
return apiClient.deleteItem(item.Id).then(function () {
|
||||||
if (options.navigate) {
|
if (options.navigate) {
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import browser from 'scripts/browser';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
import { toBoolean } from '../../utils/string.ts';
|
import { toBoolean } from '../../utils/string.ts';
|
||||||
|
|
||||||
|
@ -31,6 +32,19 @@ class AppSettings {
|
||||||
return toBoolean(this.get('enableGamepad'), false);
|
return toBoolean(this.get('enableGamepad'), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set 'Enable smooth scroll' state.
|
||||||
|
* @param {boolean|undefined} val - Flag to enable 'Enable smooth scroll' or undefined.
|
||||||
|
* @return {boolean} 'Enable smooth scroll' state.
|
||||||
|
*/
|
||||||
|
enableSmoothScroll(val) {
|
||||||
|
if (val !== undefined) {
|
||||||
|
return this.set('enableSmoothScroll', val.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return toBoolean(this.get('enableSmoothScroll'), !!browser.tizen);
|
||||||
|
}
|
||||||
|
|
||||||
enableSystemExternalPlayers(val) {
|
enableSystemExternalPlayers(val) {
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
this.set('enableSystemExternalPlayers', val.toString());
|
this.set('enableSystemExternalPlayers', val.toString());
|
||||||
|
@ -105,6 +119,19 @@ class AppSettings {
|
||||||
return parseInt(this.get('maxVideoWidth') || '0', 10) || 0;
|
return parseInt(this.get('maxVideoWidth') || '0', 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set 'Limit maximum supported video resolution' state.
|
||||||
|
* @param {boolean|undefined} val - Flag to enable 'Limit maximum supported video resolution' or undefined.
|
||||||
|
* @return {boolean} 'Limit maximum supported video resolution' state.
|
||||||
|
*/
|
||||||
|
limitSupportedVideoResolution(val) {
|
||||||
|
if (val !== undefined) {
|
||||||
|
return this.set('limitSupportedVideoResolution', val.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return toBoolean(this.get('limitSupportedVideoResolution'), false);
|
||||||
|
}
|
||||||
|
|
||||||
set(name, value, userId) {
|
set(name, value, userId) {
|
||||||
const currentValue = this.get(name, userId);
|
const currentValue = this.get(name, userId);
|
||||||
localStorage.setItem(this.#getKey(name, userId), value);
|
localStorage.setItem(this.#getKey(name, userId), value);
|
||||||
|
|
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