diff --git a/.ci/azure-pipelines-build.yml b/.ci/azure-pipelines-build.yml new file mode 100644 index 0000000000..128fe54605 --- /dev/null +++ b/.ci/azure-pipelines-build.yml @@ -0,0 +1,63 @@ +jobs: +- job: Build + displayName: 'Build' + + strategy: + matrix: + Development: + BuildConfiguration: development + Production: + BuildConfiguration: production + Standalone: + BuildConfiguration: standalone + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: NodeTool@0 + displayName: 'Install Node' + inputs: + versionSpec: '12.x' + + - task: Cache@2 + displayName: 'Check Cache' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + cacheHitVar: CACHE_RESTORED + + - script: 'yarn install --frozen-lockfile' + displayName: 'Install Dependencies' + condition: ne(variables.CACHE_RESTORED, 'true') + + - script: 'yarn build:development' + displayName: 'Build Development' + condition: eq(variables['BuildConfiguration'], 'development') + + - script: 'yarn build:production' + displayName: 'Build Production' + condition: eq(variables['BuildConfiguration'], 'production') + + - script: 'yarn build:standalone' + displayName: 'Build Standalone' + condition: eq(variables['BuildConfiguration'], 'standalone') + + - script: 'test -d dist' + displayName: 'Check Build' + + - script: 'mv dist jellyfin-web' + displayName: 'Rename Directory' + + - task: ArchiveFiles@2 + displayName: 'Archive Directory' + inputs: + rootFolderOrFile: 'jellyfin-web' + includeRootFolder: true + archiveFile: 'jellyfin-web-$(BuildConfiguration)' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Release' + inputs: + targetPath: '$(Build.SourcesDirectory)/jellyfin-web-$(BuildConfiguration).zip' + artifactName: 'jellyfin-web-$(BuildConfiguration)' diff --git a/.ci/azure-pipelines-lint.yml b/.ci/azure-pipelines-lint.yml new file mode 100644 index 0000000000..1e4bddbd04 --- /dev/null +++ b/.ci/azure-pipelines-lint.yml @@ -0,0 +1,29 @@ +jobs: +- job: Lint + displayName: 'Lint' + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: NodeTool@0 + displayName: 'Install Node' + inputs: + versionSpec: '12.x' + + - task: Cache@2 + displayName: 'Check Cache' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + cacheHitVar: CACHE_RESTORED + + - script: 'yarn install --frozen-lockfile' + displayName: 'Install Dependencies' + condition: ne(variables.CACHE_RESTORED, 'true') + + - script: 'yarn run lint --quiet' + displayName: 'Run ESLint' + + - script: 'yarn run stylelint' + displayName: 'Run Stylelint' diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml new file mode 100644 index 0000000000..bb2a09f176 --- /dev/null +++ b/.ci/azure-pipelines-package.yml @@ -0,0 +1,122 @@ +jobs: +- job: BuildPackage + displayName: 'Build Packages' + + strategy: + matrix: + CentOS: + BuildConfiguration: centos + Debian: + BuildConfiguration: debian + Fedora: + BuildConfiguration: fedora + Portable: + BuildConfiguration: portable + + pool: + vmImage: 'ubuntu-latest' + + steps: + - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment' + displayName: 'Build Dockerfile' + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) + + - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)' + displayName: 'Run Dockerfile (unstable)' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + + - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)' + displayName: 'Run Dockerfile (stable)' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Release' + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + targetPath: '$(Build.SourcesDirectory)/deployment/dist' + artifactName: 'jellyfin-web-$(BuildConfiguration)' + + - task: SSH@0 + displayName: 'Create target directory on repository server' + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + sshEndpoint: repository + runOptions: 'inline' + inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' + + - task: CopyFilesOverSSH@0 + displayName: 'Upload artifacts to repository server' + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) + inputs: + sshEndpoint: repository + sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' + contents: '**' + targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' + +- job: BuildDocker + displayName: 'Build Docker' + + pool: + vmImage: 'ubuntu-latest' + + variables: + - name: JellyfinVersion + value: 0.0.0 + + steps: + - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" + displayName: Set release version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + + - task: Docker@2 + displayName: 'Push Unstable Image' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + repository: 'jellyfin/jellyfin-web' + command: buildAndPush + buildContext: '.' + Dockerfile: 'deployment/Dockerfile.docker' + containerRegistry: Docker Hub + tags: | + unstable-$(Build.BuildNumber) + unstable + + - task: Docker@2 + displayName: 'Push Stable Image' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + inputs: + repository: 'jellyfin/jellyfin-web' + command: buildAndPush + buildContext: '.' + Dockerfile: 'deployment/Dockerfile.docker' + containerRegistry: Docker Hub + tags: | + stable-$(Build.BuildNumber) + $(JellyfinVersion) + +- job: CollectArtifacts + displayName: 'Collect Artifacts' + dependsOn: + - BuildPackage + - BuildDocker + condition: and(succeeded('BuildPackage'), succeeded('BuildDocker')) + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: SSH@0 + displayName: 'Update Unstable Repository' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + sshEndpoint: repository + runOptions: 'inline' + inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable' + + - task: SSH@0 + displayName: 'Update Stable Repository' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + inputs: + sshEndpoint: repository + runOptions: 'inline' + inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)' diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 31f00754f5..3d7a5f1cde 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -12,94 +12,6 @@ pr: - '*' jobs: -- job: Build - displayName: 'Build' - - strategy: - matrix: - Development: - BuildConfiguration: development - Production: - BuildConfiguration: production - Standalone: - BuildConfiguration: standalone - - pool: - vmImage: 'ubuntu-latest' - - steps: - - task: NodeTool@0 - displayName: 'Install Node' - inputs: - versionSpec: '12.x' - - - task: Cache@2 - displayName: 'Check Cache' - inputs: - key: 'yarn | yarn.lock' - path: 'node_modules' - cacheHitVar: CACHE_RESTORED - - - script: 'yarn install --frozen-lockfile' - displayName: 'Install Dependencies' - condition: ne(variables.CACHE_RESTORED, 'true') - - - script: 'yarn build:development' - displayName: 'Build Development' - condition: eq(variables['BuildConfiguration'], 'development') - - - script: 'yarn build:production' - displayName: 'Build Bundle' - condition: eq(variables['BuildConfiguration'], 'production') - - - script: 'yarn build:standalone' - displayName: 'Build Standalone' - condition: eq(variables['BuildConfiguration'], 'standalone') - - - script: 'test -d dist' - displayName: 'Check Build' - - - script: 'mv dist jellyfin-web' - displayName: 'Rename Directory' - - - task: ArchiveFiles@2 - displayName: 'Archive Directory' - inputs: - rootFolderOrFile: 'jellyfin-web' - includeRootFolder: true - archiveFile: 'jellyfin-web-$(BuildConfiguration)' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish Release' - inputs: - targetPath: '$(Build.SourcesDirectory)/jellyfin-web-$(BuildConfiguration).zip' - artifactName: 'jellyfin-web-$(BuildConfiguration)' - -- job: Lint - displayName: 'Lint' - - pool: - vmImage: 'ubuntu-latest' - - steps: - - task: NodeTool@0 - displayName: 'Install Node' - inputs: - versionSpec: '12.x' - - - task: Cache@2 - displayName: 'Check Cache' - inputs: - key: 'yarn | yarn.lock' - path: 'node_modules' - cacheHitVar: CACHE_RESTORED - - - script: 'yarn install --frozen-lockfile' - displayName: 'Install Dependencies' - condition: ne(variables.CACHE_RESTORED, 'true') - - - script: 'yarn run lint --quiet' - displayName: 'Run ESLint' - - - script: 'yarn run stylelint' - displayName: 'Run Stylelint' +- template: azure-pipelines-build.yml +- template: azure-pipelines-lint.yml +- template: azure-pipelines-package.yml diff --git a/.dependabot/config.yml b/.dependabot/config.yml index 02dfd18aac..4ee827471a 100644 --- a/.dependabot/config.yml +++ b/.dependabot/config.yml @@ -2,4 +2,4 @@ version: 1 update_configs: - package_manager: "javascript" directory: "/" - update_schedule: "weekly" + update_schedule: "live" diff --git a/.editorconfig b/.editorconfig index 92cf9dc590..84ba694073 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf -[json] +[*.json] indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js index 4a3fec9448..ff12e198c3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { root: true, plugins: [ + '@babel', 'promise', 'import', 'eslint-comments' @@ -27,28 +28,34 @@ module.exports = { 'plugin:compat/recommended' ], rules: { - 'block-spacing': ["error"], - 'brace-style': ["error"], - 'comma-dangle': ["error", "never"], - 'comma-spacing': ["error"], - 'eol-last': ["error"], - 'indent': ["error", 4, { "SwitchCase": 1 }], - 'keyword-spacing': ["error"], - 'max-statements-per-line': ["error"], - 'no-floating-decimal': ["error"], - 'no-multi-spaces': ["error"], - 'no-multiple-empty-lines': ["error", { "max": 1 }], - 'no-trailing-spaces': ["error"], - 'one-var': ["error", "never"], - 'quotes': ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }], - 'semi': ["error"], - 'space-before-blocks': ["error"] + 'block-spacing': ['error'], + 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], + 'comma-dangle': ['error', 'never'], + 'comma-spacing': ['error'], + 'eol-last': ['error'], + 'indent': ['error', 4, { 'SwitchCase': 1 }], + 'keyword-spacing': ['error'], + 'max-statements-per-line': ['error'], + 'no-floating-decimal': ['error'], + 'no-multi-spaces': ['error'], + 'no-multiple-empty-lines': ['error', { 'max': 1 }], + 'no-trailing-spaces': ['error'], + 'no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], + 'no-unused-vars': ['error', { 'vars': 'all', 'args': 'none', 'ignoreRestSiblings': true }], + 'one-var': ['error', 'never'], + 'padded-blocks': ['error', 'never'], + 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], + 'semi': ['error'], + 'space-before-blocks': ['error'], + 'space-infix-ops': 'error', + 'yoda': 'error' }, overrides: [ { files: [ './src/**/*.js' ], + parser: '@babel/eslint-parser', env: { node: false, amd: true, @@ -96,11 +103,10 @@ module.exports = { }, rules: { // TODO: Fix warnings and remove these rules - 'no-redeclare': ["warn"], - 'no-unused-vars': ["warn"], - 'no-useless-escape': ["warn"], + 'no-redeclare': ['off'], + 'no-useless-escape': ['off'], // TODO: Remove after ES6 migration is complete - 'import/no-unresolved': ["off"] + 'import/no-unresolved': ['off'] }, settings: { polyfills: [ @@ -130,6 +136,7 @@ module.exports = { 'Object.getOwnPropertyDescriptor', 'Object.getPrototypeOf', 'Object.keys', + 'Object.entries', 'Object.getOwnPropertyNames', 'Function.name', 'Function.hasInstance', @@ -190,4 +197,4 @@ module.exports = { } } ] -} +}; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a35eb9981f..186dbcd12c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,6 @@ .ci @dkanada @EraYaN .github @jellyfin/core -build.sh @joshuaboniface +fedora @joshuaboniface +debian @joshuaboniface +.copr @joshuaboniface deployment @joshuaboniface diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md similarity index 67% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/1-bug-report.md index 137a689e8b..15efff9954 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/1-bug-report.md @@ -1,23 +1,20 @@ --- -name: Bug report -about: Create a bug report -title: '' +name: Bug Report +about: You have noticed a general issue or regression, and would like to report it labels: bug -assignees: '' - --- -**Describe the bug** +**Describe The Bug** -**To Reproduce** +**Steps To Reproduce** 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -**Expected behavior** +**Expected Behavior** **Logs** @@ -27,9 +24,9 @@ assignees: '' **System (please complete the following information):** - - OS: [e.g. Docker, Debian, Windows] + - Platform: [e.g. Linux, Windows, iPhone, Tizen] - Browser: [e.g. Firefox, Chrome, Safari] - - Jellyfin Version: [e.g. 10.0.1] + - Jellyfin Version: [e.g. 10.6.0] -**Additional context** +**Additional Context** diff --git a/.github/ISSUE_TEMPLATE/2-playback-issue.md b/.github/ISSUE_TEMPLATE/2-playback-issue.md new file mode 100644 index 0000000000..bed7315abb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-playback-issue.md @@ -0,0 +1,22 @@ +--- +name: Playback Issue +about: You have playback issues with some files +labels: playback +--- + +**Describe The Bug** + + +**Media Information** + + +**Screenshots** + + +**System (please complete the following information):** + - Platform: [e.g. Linux, Windows, iPhone, Tizen] + - Browser: [e.g. Firefox, Chrome, Safari] + - Jellyfin Version: [e.g. 10.6.0] + +**Additional Context** + diff --git a/.github/ISSUE_TEMPLATE/3-technical-discussion.md b/.github/ISSUE_TEMPLATE/3-technical-discussion.md new file mode 100644 index 0000000000..d8140fce75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-technical-discussion.md @@ -0,0 +1,13 @@ +--- +name: Technical Discussion +about: You want to discuss technical aspects of changes you intend to make +labels: enhancement +--- + + diff --git a/.github/ISSUE_TEMPLATE/4-meta-issue.md b/.github/ISSUE_TEMPLATE/4-meta-issue.md new file mode 100644 index 0000000000..e034302e45 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-meta-issue.md @@ -0,0 +1,9 @@ +--- +name: Meta Issue +about: You want to track a number of other issues as part of a larger project +labels: meta +--- + +* [ ] Issue 1 [#123] +* [ ] Issue 2 [#456] +* [ ] ... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..2ed06fae39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://features.jellyfin.org/ + about: Please head over to our feature request hub to vote on or submit a feature. + - name: Help Or Question + url: https://matrix.to/#/#jellyfin-troubleshooting:matrix.org + about: Please join the troubleshooting Matrix channel to get some help. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000000..a62bc7522a --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,24 @@ +# Support + +Jellyfin contributors have limited availability to address general support +questions. Please make sure you are using the latest version of Jellyfin. + +When looking for support or information, please first search for your +question in these venues: + +* [Jellyfin Forum](https://forum.jellyfin.org) +* [Jellyfin Documentation](https://docs.jellyfin.org) +* [Open or **closed** issues in the organization](https://github.com/issues?q=sort%3Aupdated-desc+org%3Ajellyfin+is%3Aissue+) + +If you didn't find an answer in the resources above, contributors and other +users are reachable through the following channels: + +* #jellyfin on [Matrix](https://matrix.to/#/#jellyfin:matrix.org%22) or [IRC](https://webchat.freenode.net/#jellyfin) +* #jellyfin-troubleshooting on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [IRC](https://webchat.freenode.net/#jellyfin-troubleshooting) +* [/r/jellyfin on Reddit](https://www.reddit.com/r/jellyfin) + +GitHub issues are for tracking enhancements and bugs, not general support. + +The open source license grants you the freedom to use Jellyfin. +It does not guarantee commitments of other people's time. +Please be respectful and manage your expectations. diff --git a/.gitignore b/.gitignore index 4adf9558bf..36b843f022 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# config -config.json - # npm dist web @@ -8,4 +5,7 @@ node_modules # ide .idea -.vscode \ No newline at end of file +.vscode + +# log +yarn-error.log diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 65bb24e9fc..46c40b6c99 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -36,6 +36,8 @@ - [MrTimscampi](https://github.com/MrTimscampi) - [Sarab Singh](https://github.com/sarab97) - [GuilhermeHideki](https://github.com/GuilhermeHideki) + - [Andrei Oanca](https://github.com/OancaAndrei) + - [Cromefire_](https://github.com/cromefire) # Emby Contributors diff --git a/README.md b/README.md index e2aac6b155..ca42965dd9 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Jellyfin Web is the frontend used for most of the clients available for end user ### Dependencies -- Yarn +- [Node.js](https://nodejs.org/en/download/) +- [Yarn 1.22.4](https://classic.yarnpkg.com/en/docs/install) - Gulp-cli ### Getting Started @@ -78,4 +79,4 @@ Jellyfin Web is the frontend used for most of the clients available for end user ```sh yarn build:standalone - ``` \ No newline at end of file + ``` diff --git a/build.yaml b/build.yaml index fe1633faec..a73be8ec43 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin-web" -version: "10.6.0" +version: "10.7.0" packages: - debian.all - fedora.all diff --git a/bump_version b/bump_version index bc8288b829..4e6aa6f792 100755 --- a/bump_version +++ b/bump_version @@ -4,6 +4,7 @@ set -o errexit set -o pipefail +set -o xtrace usage() { echo -e "bump_version - increase the shared version and generate changelogs" @@ -23,10 +24,7 @@ build_file="./build.yaml" new_version="$1" # Parse the version from shared version file -old_version="$( - grep "appVersion" ${shared_version_file} | head -1 \ - | sed -E 's/var appVersion = "([0-9\.]+)";/\1/' -)" +old_version="$( grep "appVersion" ${shared_version_file} | head -1 | sed -E "s/var appVersion = '([0-9\.]+)';/\1/" | tr -d '[:space:]' )" echo "Old version in appHost is: $old_version" # Set the shared version to the specified new_version @@ -34,11 +32,8 @@ old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' cha new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file} -old_version="$( - grep "version:" ${build_file} \ - | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/' -)" -echo "Old version in ${build_file}: $old_version`" +old_version="$( grep "version:" ${build_file} | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/' )" +echo "Old version in ${build_file}: ${old_version}" # Set the build.yaml version to the specified new_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars @@ -54,7 +49,7 @@ fi debian_changelog_file="debian/changelog" debian_changelog_temp="$( mktemp )" # Create new temp file with our changelog -echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium +echo -e "jellyfin-web (${new_version_deb}) unstable; urgency=medium * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version} @@ -65,15 +60,15 @@ cat ${debian_changelog_file} >> ${debian_changelog_temp} 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.spec" +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.spec.tmp" +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.spec "/^%changelog/" # produces xx00 xx01 +csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01 # Update the version in xx00 sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00 # Remove the header from xx01 @@ -92,5 +87,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file} rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir} # Stage the changed files for commit -git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile* +git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} git status diff --git a/debian/changelog b/debian/changelog index 50966c3a01..ab5e13196d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +jellyfin-web (10.7.0-1) unstable; urgency=medium + + * Forthcoming stable release + + -- Jellyfin Packaging Team 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 diff --git a/deployment/Dockerfile.centos.all b/deployment/Dockerfile.centos similarity index 51% rename from deployment/Dockerfile.centos.all rename to deployment/Dockerfile.centos index 93bf8d6988..64cffc7eed 100644 --- a/deployment/Dockerfile.centos.all +++ b/deployment/Dockerfile.centos @@ -1,7 +1,9 @@ FROM centos:7 + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -9,19 +11,19 @@ ENV IS_DOCKER=YES # Prepare CentOS environment RUN yum update -y \ - && yum install -y epel-release \ - && yum install -y @buildsys-build rpmdevtools git yum-plugins-core nodejs-yarn autoconf automake glibc-devel + && yum install -y epel-release \ + && yum install -y @buildsys-build rpmdevtools git yum-plugins-core nodejs-yarn autoconf automake glibc-devel # Install recent NodeJS and Yarn RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ - && rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ - && yum install -y yarn + && rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ + && yum install -y yarn # Link to build script -RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.all /build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh -VOLUME ${SOURCE_DIR}/ +VOLUME ${SOURCE_DIR} -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${ARTIFACT_DIR} ENTRYPOINT ["/build.sh"] diff --git a/deployment/Dockerfile.debian.all b/deployment/Dockerfile.debian similarity index 69% rename from deployment/Dockerfile.debian.all rename to deployment/Dockerfile.debian index 54281a5eb4..80ea103b92 100644 --- a/deployment/Dockerfile.debian.all +++ b/deployment/Dockerfile.debian @@ -1,7 +1,9 @@ FROM debian:10 + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -10,16 +12,16 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y debhelper mmv npm git + && apt-get install -y debhelper mmv npm git # Prepare Yarn RUN npm install -g yarn # Link to build script -RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.all /build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh -VOLUME ${SOURCE_DIR}/ +VOLUME ${SOURCE_DIR} -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${ARTIFACT_DIR} ENTRYPOINT ["/build.sh"] diff --git a/deployment/Dockerfile.docker b/deployment/Dockerfile.docker new file mode 100644 index 0000000000..5d12938185 --- /dev/null +++ b/deployment/Dockerfile.docker @@ -0,0 +1,11 @@ +FROM node:alpine + +ARG SOURCE_DIR=/src +ARG ARTIFACT_DIR=/jellyfin-web + +RUN apk add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python + +WORKDIR ${SOURCE_DIR} +COPY . . + +RUN yarn install && mv dist ${ARTIFACT_DIR} diff --git a/deployment/Dockerfile.fedora.all b/deployment/Dockerfile.fedora similarity index 56% rename from deployment/Dockerfile.fedora.all rename to deployment/Dockerfile.fedora index d47f4ff4da..e6026985d1 100644 --- a/deployment/Dockerfile.fedora.all +++ b/deployment/Dockerfile.fedora @@ -1,7 +1,9 @@ FROM fedora:31 + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -9,13 +11,13 @@ ENV IS_DOCKER=YES # Prepare Fedora environment RUN dnf update -y \ - && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs-yarn autoconf automake glibc-devel + && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs-yarn autoconf automake glibc-devel # Link to build script -RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.all /build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh -VOLUME ${SOURCE_DIR}/ +VOLUME ${SOURCE_DIR} -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${ARTIFACT_DIR} ENTRYPOINT ["/build.sh"] diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index e0d1f45265..0d4c79bb2f 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -1,16 +1,17 @@ FROM debian:10 + # 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 mmv npm git + && apt-get install -y mmv npm git # Prepare Yarn RUN npm install -g yarn @@ -18,8 +19,8 @@ RUN npm install -g yarn # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh -VOLUME ${SOURCE_DIR}/ +VOLUME ${SOURCE_DIR} -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${ARTIFACT_DIR} ENTRYPOINT ["/build.sh"] diff --git a/deployment/build.centos b/deployment/build.centos new file mode 100755 index 0000000000..25d1e8c175 --- /dev/null +++ b/deployment/build.centos @@ -0,0 +1,41 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + +# move to source directory +pushd ${SOURCE_DIR} + +cp -a yarn.lock /tmp/yarn.lock + +# 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 <>jellyfin-web.spec +* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team +- 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/yarn.lock yarn.lock + +popd diff --git a/deployment/build.centos.all b/deployment/build.centos.all deleted file mode 100755 index 8c2cec6d33..0000000000 --- a/deployment/build.centos.all +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -#= CentOS 7 all .rpm - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -cp -a yarn.lock /tmp/yarn.lock - -# Build RPM -make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS -rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm - -# Move the artifacts out -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/yarn.lock yarn.lock - -popd diff --git a/deployment/build.debian b/deployment/build.debian new file mode 100755 index 0000000000..9fe3fb51e9 --- /dev/null +++ b/deployment/build.debian @@ -0,0 +1,39 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + +# move to source directory +pushd ${SOURCE_DIR} + +cp -a yarn.lock /tmp/yarn.lock + +# 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 <changelog +jellyfin-web (${BUILD_ID}-unstable) unstable; urgency=medium + + * Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID} + + -- Jellyfin Packaging Team $( 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/yarn.lock yarn.lock + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.debian.all b/deployment/build.debian.all deleted file mode 100755 index 8d617a288e..0000000000 --- a/deployment/build.debian.all +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -#= Debian/Ubuntu all .deb - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -cp -a yarn.lock /tmp/yarn.lock - -# 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/yarn.lock yarn.lock - -if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} -fi - -popd diff --git a/deployment/build.fedora b/deployment/build.fedora new file mode 100755 index 0000000000..60d270ed6c --- /dev/null +++ b/deployment/build.fedora @@ -0,0 +1,41 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + +# move to source directory +pushd ${SOURCE_DIR} + +cp -a yarn.lock /tmp/yarn.lock + +# 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 <>jellyfin-web.spec +* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team +- 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/yarn.lock yarn.lock + +popd diff --git a/deployment/build.fedora.all b/deployment/build.fedora.all deleted file mode 100755 index 4ba12f35ea..0000000000 --- a/deployment/build.fedora.all +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -#= Fedora 29+ all .rpm - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -cp -a yarn.lock /tmp/yarn.lock - -# Build RPM -make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS -rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm - -# Move the artifacts out -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/yarn.lock yarn.lock - -popd diff --git a/deployment/build.portable b/deployment/build.portable index c4cbe927e4..873c145fab 100755 --- a/deployment/build.portable +++ b/deployment/build.portable @@ -1,25 +1,27 @@ #!/bin/bash -#= Portable .NET DLL .tar.gz - set -o errexit set -o xtrace -# Move to source directory +# move to source directory pushd ${SOURCE_DIR} -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" +# 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 +# build archives npx yarn install -mv dist/ jellyfin-web_${version} +mv dist jellyfin-web_${version} tar -czf jellyfin-web_${version}_portable.tar.gz jellyfin-web_${version} -rm -rf dist/ +rm -rf dist -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +# 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} diff --git a/fedora/jellyfin-web.spec b/fedora/jellyfin-web.spec index dbc0bd0efa..b8c77f2a1f 100644 --- a/fedora/jellyfin-web.spec +++ b/fedora/jellyfin-web.spec @@ -1,7 +1,7 @@ %global debug_package %{nil} Name: jellyfin-web -Version: 10.6.0 +Version: 10.7.0 Release: 1%{?dist} Summary: The Free Software Media System web client License: GPLv3 @@ -12,7 +12,7 @@ Source0: jellyfin-web-%{version}.tar.gz %if 0%{?centos} BuildRequires: yarn %else -BuildRequires nodejs-yarn +BuildRequires: nodejs-yarn %endif BuildArch: noarch @@ -39,5 +39,7 @@ mv dist %{buildroot}%{_datadir}/jellyfin-web %{_datadir}/licenses/jellyfin/LICENSE %changelog +* Mon Jul 27 2020 Jellyfin Packaging Team +- Forthcoming stable release * Mon Mar 23 2020 Jellyfin Packaging Team - Forthcoming stable release diff --git a/gulpfile.js b/gulpfile.js index 6c33167386..8b407ec2aa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -45,7 +45,7 @@ const options = { query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'] }, copy: { - query: ['src/**/*.json', 'src/**/*.ico'] + query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.mp3'] }, injectBundle: { query: 'src/index.html' @@ -181,16 +181,15 @@ function copy(query) { .pipe(browserSync.stream()); } -function copyIndex() { - return src(options.injectBundle.query, { base: './src/' }) - .pipe(dest('dist/')) - .pipe(browserSync.stream()); -} - function injectBundle() { return src(options.injectBundle.query, { base: './src/' }) .pipe(inject( - src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { relative: true } + src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { + relative: true, + transform: function (filepath) { + return ``; + } + } )) .pipe(dest('dist/')) .pipe(browserSync.stream()); @@ -200,6 +199,6 @@ function build(standalone) { return series(clean, parallel(javascript, apploader(standalone), webpack, css, html, images, copy)); } -exports.default = series(build(false), copyIndex); +exports.default = series(build(false), injectBundle); exports.standalone = series(build(true), injectBundle); exports.serve = series(exports.standalone, serve); diff --git a/package.json b/package.json index ffbbc02954..1d7cf770d3 100644 --- a/package.json +++ b/package.json @@ -5,27 +5,30 @@ "repository": "https://github.com/jellyfin/jellyfin-web", "license": "GPL-2.0-or-later", "devDependencies": { - "@babel/core": "^7.9.6", - "@babel/plugin-transform-modules-amd": "^7.9.6", + "@babel/core": "^7.11.0", + "@babel/eslint-parser": "^7.11.0", + "@babel/eslint-plugin": "^7.11.0", + "@babel/plugin-proposal-class-properties": "^7.10.1", + "@babel/plugin-proposal-private-methods": "^7.10.1", + "@babel/plugin-transform-modules-amd": "^7.10.5", "@babel/polyfill": "^7.8.7", - "@babel/preset-env": "^7.8.6", - "autoprefixer": "^9.7.6", + "@babel/preset-env": "^7.11.0", + "autoprefixer": "^9.8.6", "babel-loader": "^8.0.6", - "browser-sync": "^2.26.7", - "clean-webpack-plugin": "^3.0.0", + "browser-sync": "^2.26.12", "copy-webpack-plugin": "^5.1.1", - "css-loader": "^3.4.2", + "css-loader": "^4.2.0", "cssnano": "^4.1.10", "del": "^5.1.0", - "eslint": "^6.8.0", + "eslint": "^7.6.0", "eslint-plugin-compat": "^3.5.1", - "eslint-plugin-eslint-comments": "^3.1.2", - "eslint-plugin-import": "^2.20.2", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.21.2", "eslint-plugin-promise": "^4.2.1", "file-loader": "^6.0.0", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", - "gulp-cli": "^2.2.0", + "gulp-cli": "^2.3.0", "gulp-concat": "^2.6.1", "gulp-htmlmin": "^5.0.1", "gulp-if": "^3.0.0", @@ -35,52 +38,49 @@ "gulp-postcss": "^8.0.0", "gulp-sass": "^4.0.2", "gulp-sourcemaps": "^2.6.5", - "gulp-terser": "^1.2.0", + "gulp-terser": "^1.3.0", "html-webpack-plugin": "^4.3.0", "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", "postcss-preset-env": "^6.7.0", "style-loader": "^1.1.3", - "stylelint": "^13.3.3", + "stylelint": "^13.6.1", "stylelint-config-rational-order": "^0.1.2", "stylelint-no-browser-hacks": "^1.2.1", - "stylelint-order": "^4.0.0", - "webpack": "^4.41.5", - "webpack-cli": "^3.3.10", - "webpack-concat-plugin": "^3.0.0", - "webpack-dev-server": "^3.10.3", + "stylelint-order": "^4.1.0", + "webpack": "^4.44.1", "webpack-merge": "^4.2.2", "webpack-stream": "^5.2.1" }, "dependencies": { "alameda": "^1.4.0", + "blurhash": "^1.1.3", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "core-js": "^3.6.5", - "date-fns": "^2.12.0", - "document-register-element": "^1.14.3", - "fast-text-encoding": "^1.0.1", + "date-fns": "^2.15.0", + "epubjs": "^0.3.85", + "fast-text-encoding": "^1.0.3", "flv.js": "^1.5.0", "headroom.js": "^0.11.0", - "hls.js": "^0.13.1", - "howler": "^2.1.3", - "intersection-observer": "^0.10.0", - "jellyfin-apiclient": "^1.1.1", + "hls.js": "^0.14.7", + "howler": "^2.2.0", + "intersection-observer": "^0.11.0", + "jellyfin-apiclient": "^1.4.1", "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", - "jquery": "^3.5.0", - "jstree": "^3.3.7", + "jquery": "^3.5.1", + "jstree": "^3.3.10", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", "material-design-icons-iconfont": "^5.0.1", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", - "query-string": "^6.11.1", + "query-string": "^6.13.1", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.2", - "shaka-player": "^2.5.10", "sortablejs": "^1.10.2", - "swiper": "^5.3.7", + "swiper": "^5.4.5", "webcomponents.js": "^0.7.24", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.2.0" }, "babel": { "presets": [ @@ -89,26 +89,214 @@ "overrides": [ { "test": [ + "src/components/accessSchedule/accessSchedule.js", + "src/components/actionSheet/actionSheet.js", + "src/components/activitylog.js", + "src/components/alert.js", + "src/components/alphaPicker/alphaPicker.js", + "src/components/appFooter/appFooter.js", "src/components/autoFocuser.js", + "src/components/backdrop/backdrop.js", "src/components/cardbuilder/cardBuilder.js", - "src/components/filedownloader.js", + "src/components/cardbuilder/chaptercardbuilder.js", + "src/components/cardbuilder/peoplecardbuilder.js", + "src/components/channelMapper/channelMapper.js", + "src/components/collectionEditor/collectionEditor.js", + "src/components/confirm/confirm.js", + "src/components/dialog/dialog.js", + "src/components/dialogHelper/dialogHelper.js", + "src/components/directorybrowser/directorybrowser.js", + "src/components/displaySettings/displaySettings.js", + "src/components/favoriteitems.js", + "src/components/fetchhelper.js", + "src/components/filterdialog/filterdialog.js", + "src/components/groupedcards.js", + "src/components/homeScreenSettings/homeScreenSettings.js", + "src/components/homesections/homesections.js", + "src/components/htmlMediaHelper.js", + "src/components/imageOptionsEditor/imageOptionsEditor.js", "src/components/images/imageLoader.js", - "src/components/lazyloader/lazyloader-intersectionobserver.js", + "src/components/imageDownloader/imageDownloader.js", + "src/components/imageeditor/imageeditor.js", + "src/components/imageUploader/imageUploader.js", + "src/components/indicators/indicators.js", + "src/components/itemContextMenu.js", + "src/components/itemHelper.js", + "src/components/itemidentifier/itemidentifier.js", + "src/components/itemMediaInfo/itemMediaInfo.js", + "src/components/lazyLoader/lazyLoaderIntersectionObserver.js", + "src/components/libraryoptionseditor/libraryoptionseditor.js", + "src/components/listview/listview.js", + "src/components/loading/loading.js", + "src/components/maintabsmanager.js", + "src/components/mediainfo/mediainfo.js", + "src/components/mediaLibraryCreator/mediaLibraryCreator.js", + "src/components/mediaLibraryEditor/mediaLibraryEditor.js", + "src/components/metadataEditor/metadataEditor.js", + "src/components/metadataEditor/personEditor.js", + "src/components/multiSelect/multiSelect.js", + "src/components/nowPlayingBar/nowPlayingBar.js", + "src/components/playback/brightnessosd.js", "src/components/playback/mediasession.js", + "src/components/playback/nowplayinghelper.js", + "src/components/playback/playbackorientation.js", + "src/components/playback/playbackmanager.js", + "src/components/playback/playerSelectionMenu.js", + "src/components/playback/playersettingsmenu.js", + "src/components/playback/playmethodhelper.js", + "src/components/playback/playqueuemanager.js", + "src/components/playback/remotecontrolautoplay.js", + "src/components/playback/volumeosd.js", + "src/components/playbackSettings/playbackSettings.js", + "src/components/playerstats/playerstats.js", + "src/components/playlisteditor/playlisteditor.js", + "src/components/playmenu.js", + "src/components/prompt/prompt.js", + "src/components/refreshdialog/refreshdialog.js", "src/components/sanatizefilename.js", "src/components/scrollManager.js", + "src/plugins/htmlVideoPlayer/plugin.js", + "src/components/search/searchfields.js", + "src/components/search/searchresults.js", + "src/components/settingshelper.js", + "src/components/shortcuts.js", + "src/components/subtitlesettings/subtitleappearancehelper.js", + "src/components/subtitlesettings/subtitlesettings.js", + "src/components/syncPlay/groupSelectionMenu.js", + "src/components/syncPlay/playbackPermissionManager.js", + "src/components/syncPlay/syncPlayManager.js", + "src/components/syncPlay/timeSyncManager.js", + "src/components/toast/toast.js", + "src/components/upnextdialog/upnextdialog.js", + "src/components/viewContainer.js", + "src/controllers/session/addServer/index.js", + "src/controllers/session/forgotPassword/index.js", + "src/controllers/session/redeemPassword/index.js", + "src/controllers/session/login/index.js", + "src/controllers/session/selectServer/index.js", + "src/controllers/dashboard/apikeys.js", + "src/controllers/dashboard/dashboard.js", + "src/controllers/dashboard/devices/device.js", + "src/controllers/dashboard/devices/devices.js", + "src/controllers/dashboard/dlna/profile.js", + "src/controllers/dashboard/dlna/profiles.js", + "src/controllers/dashboard/dlna/settings.js", + "src/controllers/dashboard/encodingsettings.js", + "src/controllers/dashboard/general.js", + "src/controllers/dashboard/librarydisplay.js", + "src/controllers/dashboard/logs.js", + "src/controllers/music/musicalbums.js", + "src/controllers/music/musicartists.js", + "src/controllers/music/musicgenres.js", + "src/controllers/music/musicplaylists.js", + "src/controllers/music/musicrecommended.js", + "src/controllers/music/songs.js", + "src/controllers/dashboard/mediaLibrary.js", + "src/controllers/dashboard/metadataImages.js", + "src/controllers/dashboard/metadatanfo.js", + "src/controllers/dashboard/networking.js", + "src/controllers/dashboard/notifications/notification.js", + "src/controllers/dashboard/notifications/notifications.js", + "src/controllers/dashboard/playback.js", + "src/controllers/dashboard/plugins/repositories/index.js", + "src/controllers/dashboard/scheduledtasks/scheduledtask.js", + "src/controllers/dashboard/scheduledtasks/scheduledtasks.js", + "src/controllers/dashboard/serveractivity.js", + "src/controllers/dashboard/streaming.js", + "src/controllers/dashboard/users/useredit.js", + "src/controllers/dashboard/users/userlibraryaccess.js", + "src/controllers/dashboard/users/usernew.js", + "src/controllers/dashboard/users/userparentalcontrol.js", + "src/controllers/dashboard/users/userpasswordpage.js", + "src/controllers/dashboard/users/userprofilespage.js", + "src/controllers/edititemmetadata.js", + "src/controllers/favorites.js", + "src/controllers/hometab.js", + "src/controllers/playback/nowplaying.js", + "src/controllers/playback/videoosd.js", + "src/controllers/itemDetails/index.js", + "src/controllers/playback/queue/index.js", + "src/controllers/playback/video/index.js", + "src/controllers/searchpage.js", + "src/controllers/livetvtuner.js", + "src/controllers/livetvstatus.js", + "src/controllers/livetvguideprovider.js", + "src/controllers/livetvsettings.js", + "src/controllers/shows/episodes.js", + "src/controllers/shows/tvgenres.js", + "src/controllers/shows/tvlatest.js", + "src/controllers/shows/tvrecommended.js", + "src/controllers/shows/tvshows.js", + "src/controllers/shows/tvstudios.js", + "src/controllers/shows/tvupcoming.js", + "src/controllers/user/display/index.js", + "src/controllers/user/home/index.js", + "src/controllers/user/menu/index.js", + "src/controllers/user/playback/index.js", + "src/controllers/user/profile/index.js", + "src/controllers/user/subtitles/index.js", + "src/controllers/wizard/finish/index.js", + "src/controllers/wizard/remote/index.js", + "src/controllers/wizard/settings/index.js", + "src/controllers/wizard/start/index.js", + "src/controllers/wizard/user/index.js", + "src/elements/emby-button/emby-button.js", + "src/elements/emby-button/paper-icon-button-light.js", + "src/elements/emby-checkbox/emby-checkbox.js", + "src/elements/emby-collapse/emby-collapse.js", + "src/elements/emby-input/emby-input.js", + "src/elements/emby-itemrefreshindicator/emby-itemrefreshindicator.js", + "src/elements/emby-itemscontainer/emby-itemscontainer.js", + "src/elements/emby-playstatebutton/emby-playstatebutton.js", + "src/elements/emby-programcell/emby-programcell.js", + "src/elements/emby-progressbar/emby-progressbar.js", + "src/elements/emby-progressring/emby-progressring.js", + "src/elements/emby-radio/emby-radio.js", + "src/elements/emby-ratingbutton/emby-ratingbutton.js", + "src/elements/emby-scrollbuttons/emby-scrollbuttons.js", + "src/elements/emby-scroller/emby-scroller.js", + "src/elements/emby-select/emby-select.js", + "src/elements/emby-slider/emby-slider.js", + "src/elements/emby-tabs/emby-tabs.js", + "src/elements/emby-textarea/emby-textarea.js", + "src/elements/emby-toggle/emby-toggle.js", + "src/plugins/backdropScreensaver/plugin.js", + "src/plugins/bookPlayer/plugin.js", + "src/plugins/bookPlayer/tableOfContents.js", + "src/plugins/photoPlayer/plugin.js", + "src/plugins/youtubePlayer/plugin.js", + "src/scripts/alphanumericshortcuts.js", + "src/scripts/autoBackdrops.js", + "src/scripts/browser.js", + "src/scripts/datetime.js", + "src/scripts/deleteHelper.js", "src/scripts/dfnshelper.js", "src/scripts/dom.js", + "src/scripts/editorsidebar.js", + "src/scripts/fileDownloader.js", "src/scripts/filesystem.js", + "src/scripts/globalize.js", "src/scripts/imagehelper.js", "src/scripts/inputManager.js", - "src/scripts/keyboardnavigation.js", + "src/scripts/autoThemes.js", + "src/scripts/themeManager.js", + "src/scripts/keyboardNavigation.js", + "src/scripts/libraryBrowser.js", + "src/scripts/mouseManager.js", + "src/scripts/multiDownload.js", + "src/scripts/playlists.js", + "src/scripts/routes.js", "src/scripts/settings/appSettings.js", "src/scripts/settings/userSettings.js", - "src/scripts/settings/webSettings.js" + "src/scripts/settings/webSettings.js", + "src/scripts/taskbutton.js", + "src/scripts/themeLoader.js", + "src/scripts/touchHelper.js" ], "plugins": [ - "@babel/plugin-transform-modules-amd" + "@babel/plugin-transform-modules-amd", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-private-methods" ] } ] @@ -118,7 +306,7 @@ "last 2 Chrome versions", "last 2 ChromeAndroid versions", "last 2 Safari versions", - "last 2 iOS versions", + "iOS > 10", "last 2 Edge versions", "Chrome 27", "Chrome 38", @@ -126,6 +314,7 @@ "Chrome 53", "Chrome 56", "Chrome 63", + "Edge 18", "Firefox ESR" ], "scripts": { diff --git a/postcss.config.js b/postcss.config.js index 0e19ca6e10..bd1651fa19 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -5,8 +5,10 @@ const cssnano = require('cssnano'); const config = () => ({ plugins: [ + // Explicitly specify browserslist to override ones from node_modules + // For example, Swiper has it in its package.json postcssPresetEnv({browsers: packageConfig.browserslist}), - autoprefixer(), + autoprefixer({overrideBrowserslist: packageConfig.browserslist}), cssnano() ] }); diff --git a/scripts/scdup.py b/scripts/scdup.py index 468e31f14a..9b9ddf6466 100644 --- a/scripts/scdup.py +++ b/scripts/scdup.py @@ -15,6 +15,8 @@ print(langlst) input('press enter to continue') keysus = [] +missing = [] + with open(langdir + '/' + 'en-us.json') as en: langus = json.load(en) for key in langus: @@ -32,10 +34,19 @@ for lang in langlst: for key in langjson: if key in keysus: langjnew[key] = langjson[key] + elif key not in missing: + missing.append(key) f.seek(0) f.write(json.dumps(langjnew, indent=inde, sort_keys=False, ensure_ascii=False)) f.write('\n') f.truncate() f.close() +print(missing) +print('LENGTH: ' + str(len(missing))) +with open('missing.txt', 'w') as out: + for item in missing: + out.write(item + '\n') + out.close() + print('DONE') diff --git a/scripts/scgen.py b/scripts/scgen.py index 0d831426e6..12af27320a 100644 --- a/scripts/scgen.py +++ b/scripts/scgen.py @@ -34,7 +34,7 @@ for lang in langlst: print(dep) print('LENGTH: ' + str(len(dep))) -with open('scout.txt', 'w') as out: +with open('unused.txt', 'w') as out: for item in dep: out.write(item + '\n') out.close() diff --git a/src/apikeys.html b/src/apikeys.html index 7ca490724b..3cb7cd6de1 100644 --- a/src/apikeys.html +++ b/src/apikeys.html @@ -4,18 +4,19 @@

${HeaderApiKeys}

${HeaderApiKeysHelp}


+ - - - - + + + + diff --git a/src/assets/audio/silence.mp3 b/src/assets/audio/silence.mp3 new file mode 100644 index 0000000000..29dbef2185 Binary files /dev/null and b/src/assets/audio/silence.mp3 differ diff --git a/src/assets/css/dashboard.css b/src/assets/css/dashboard.css index 894d7332f4..48e6fe807e 100644 --- a/src/assets/css/dashboard.css +++ b/src/assets/css/dashboard.css @@ -235,6 +235,15 @@ div[data-role=controlgroup] a.ui-btn-active { width: 50%; } +.localUsers .cardText-secondary { + white-space: pre-wrap; + height: 3em; +} + +.customCssContainer textarea { + resize: none; +} + @media all and (min-width: 70em) { .dashboardSections { -webkit-flex-wrap: wrap; diff --git a/src/assets/css/flexstyles.css b/src/assets/css/flexstyles.css index a5a479f2f5..429ed7a650 100644 --- a/src/assets/css/flexstyles.css +++ b/src/assets/css/flexstyles.css @@ -30,6 +30,10 @@ align-items: flex-start; } +.align-items-flex-end { + align-items: flex-end; +} + .justify-content-center { justify-content: center; } @@ -38,6 +42,10 @@ justify-content: flex-end; } +.justify-content-space-between { + justify-content: space-between; +} + .flex-wrap-wrap { flex-wrap: wrap; } diff --git a/src/assets/css/ios.css b/src/assets/css/ios.css index 57de0c5fdd..2b61f49a4e 100644 --- a/src/assets/css/ios.css +++ b/src/assets/css/ios.css @@ -1,8 +1,3 @@ html { font-size: 82% !important; } - -.formDialogFooter { - position: static !important; - margin: 0 -1em !important; -} diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 82e704f074..047ae0a1c6 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -24,10 +24,6 @@ padding-top: 7em !important; } -.layout-mobile .libraryPage { - padding-top: 4em !important; -} - .itemDetailPage { padding-top: 0 !important; } @@ -164,6 +160,7 @@ display: flex; flex-direction: column; contain: layout style paint; + transition: background ease-in-out 0.5s; } .hiddenViewMenuBar .skinHeader { @@ -178,6 +175,10 @@ width: 100%; } +.layout-tv .sectionTabs { + width: 55%; +} + .selectedMediaFolder { background-color: #f2f2f2 !important; } @@ -272,7 +273,7 @@ } } -@media all and (max-width: 84em) { +@media all and (max-width: 100em) { .withSectionTabs .headerTop { padding-bottom: 0.55em; } @@ -280,9 +281,13 @@ .sectionTabs { font-size: 83.5%; } + + .layout-tv .sectionTabs { + width: 100%; + } } -@media all and (min-width: 84em) { +@media all and (min-width: 100em) { .headerTop { padding: 0.8em 0.8em; } @@ -438,12 +443,14 @@ background-repeat: no-repeat; background-position: center; background-attachment: fixed; - height: 50vh; + height: 40vh; position: relative; + animation: backdrop-fadein 800ms ease-in normal both; } .layout-mobile .itemBackdrop { background-attachment: scroll; + height: 26.5vh; } .layout-desktop .itemBackdrop::after, @@ -463,10 +470,20 @@ .detailPageContent { display: flex; flex-direction: column; - padding-left: 2%; + padding-left: 32.45vw; padding-right: 2%; } +.layout-mobile .detailPageContent { + padding-left: 5%; + padding-right: 5%; +} + +.layout-desktop .detailPageContent .emby-scroller, +.layout-tv .detailPageContent .emby-scroller { + margin-left: 0; +} + .layout-desktop .noBackdrop .detailPageContent, .layout-tv .noBackdrop .detailPageContent { margin-top: 2.5em; @@ -477,6 +494,10 @@ margin-top: 0; } +.detailSectionContent a { + color: inherit; +} + .personBackdrop { background-size: contain; } @@ -495,7 +516,23 @@ .parentName { display: block; - margin-bottom: 0.5em; + margin: 0 0 0; +} + +.layout-mobile .parentName { + margin: 0.6em 0 0; +} + +.musicParentName { + margin: 0.15em 0 0.2em; +} + +.layout-mobile .musicParentName { + margin: -0.25em 0 0.25em; +} + +.layout-mobile .itemExternalLinks { + display: none; } .mainDetailButtons { @@ -503,8 +540,6 @@ -webkit-box-align: center; -webkit-align-items: center; align-items: center; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; margin: 1em 0; } @@ -520,6 +555,35 @@ font-weight: 600; } +.itemName.originalTitle { + margin: 0.2em 0 0.2em; +} + +.itemName.parentNameLast { + margin: 0 0 0; +} + +.layout-mobile .itemName.parentNameLast { + margin: 0.4em 0 0.4em; +} + +.layout-mobile h1.itemName, +.layout-mobile h1.parentName { + font-size: 1.6em; +} + +.itemName.parentNameLast.withOriginalTitle { + margin: 0 0 0; +} + +.layout-mobile .itemName.parentNameLast.withOriginalTitle { + margin: 0.6em 0 0; +} + +.layout-mobile .itemName.originalTitle { + margin: 0.5em 0 0.5em; +} + .nameContainer { display: flex; flex-direction: column; @@ -546,6 +610,19 @@ text-align: center; } +.layout-mobile .mainDetailButtons { + margin-top: 1em; + margin-bottom: 0.5em; +} + +.subtitle { + margin: 0.15em 0 0.2em; +} + +.layout-mobile .subtitle { + margin: 0.2em 0 0.2em; +} + .detailPagePrimaryContainer { display: flex; align-items: center; @@ -556,7 +633,7 @@ .layout-mobile .detailPagePrimaryContainer { display: block; position: relative; - top: 0; + padding: 0.5em 3.3% 0.5em; } .layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, @@ -566,13 +643,14 @@ padding-left: 32.45vw; } -.layout-desktop .detailSticky, -.layout-tv .detailSticky { +.layout-desktop .detailRibbon, +.layout-tv .detailRibbon { margin-top: -7.2em; + height: 7.2em; } -.layout-desktop .noBackdrop .detailSticky, -.layout-tv .noBackdrop .detailSticky { +.layout-desktop .noBackdrop .detailRibbon, +.layout-tv .noBackdrop .detailRibbon { margin-top: 0; } @@ -584,6 +662,9 @@ white-space: nowrap; text-overflow: ellipsis; text-align: left; + min-width: 0; + max-width: 100%; + overflow: hidden; } .layout-mobile .infoText { @@ -594,12 +675,29 @@ margin: 1.25em 0; } -.detailImageContainer { - position: relative; - margin-top: -25vh; +.layout-mobile .detailPageSecondaryContainer { + margin: 1em 0; +} + +.layout-mobile .detailImageContainer { + display: none; +} + +.detailImageContainer .card { + position: absolute; + top: 50%; float: left; width: 25vw; z-index: 3; + transform: translateY(-50%); +} + +.detailImageContainer .card.backdropCard { + top: 35%; +} + +.detailImageContainer .card.squareCard { + top: 40%; } .layout-desktop .noBackdrop .detailImageContainer, @@ -612,11 +710,11 @@ } .detailLogo { - width: 30vw; - height: 25vh; + width: 25vw; + height: 16vh; position: absolute; top: 10vh; - right: 20vw; + right: 25vw; background-size: contain; } @@ -641,7 +739,8 @@ div.itemDetailGalleryLink.defaultCardBackground { } .itemDetailGalleryLink.defaultCardBackground { - height: 23vw; /* Dirty hack to get it to look somewhat square. Less than ideal. */ + /* Dirty hack to get it to look somewhat square. Less than ideal. */ + height: 23vw; } .itemDetailGalleryLink.defaultCardBackground > .material-icons { @@ -655,14 +754,19 @@ div.itemDetailGalleryLink.defaultCardBackground { position: relative; } - .layout-desktop .detailPageWrapperContainer, - .layout-tv .detailPageWrapperContainer { - margin-top: 7.2em; + .layout-desktop .itemBackdrop, + .layout-tv .itemBackdrop { + height: 40vh; } - .layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, - .layout-desktop #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer { - padding-left: 3.3%; + .layout-desktop .detailPageWrapperContainer, + .layout-tv .detailPageWrapperContainer { + margin-top: 0.1em; + } + + .layout-desktop .detailImageContainer .card, + .layout-tv .detailImageContainer .card { + top: 10%; } .btnPlaySimple { @@ -678,12 +782,12 @@ div.itemDetailGalleryLink.defaultCardBackground { .emby-button.detailFloatingButton { position: absolute; - background-color: rgba(0, 0, 0, 0.5) !important; - z-index: 1; - top: 50%; - left: 50%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 3; + top: 100%; + left: 90%; margin: -2.2em 0 0 -2.2em; - padding: 0.4em !important; + padding: 0.4em; color: rgba(255, 255, 255, 0.76); } @@ -693,16 +797,12 @@ div.itemDetailGalleryLink.defaultCardBackground { @media all and (max-width: 62.5em) { .parentName { - margin-bottom: 1em; + margin-bottom: 0; } .itemDetailPage { padding-top: 0 !important; } - - .detailimg-hidemobile { - display: none; - } } @media all and (min-width: 31.25em) { @@ -797,9 +897,9 @@ div.itemDetailGalleryLink.defaultCardBackground { } .detailImageProgressContainer { - position: absolute; bottom: 0; - width: 22.786458333333332vw; + margin-top: -0.4vw; + width: 100%; } .detailButton-text { @@ -818,21 +918,7 @@ div.itemDetailGalleryLink.defaultCardBackground { } } -@media all and (min-width: 62.5em) { - .headerTop { - padding-left: 0.8em; - padding-right: 0.8em; - } - - .headerTabs { - align-self: center; - width: auto; - align-items: center; - justify-content: center; - margin-top: -4.2em; - position: relative; - } - +@media all and (min-width: 100em) { .detailFloatingButton { display: none !important; } @@ -866,6 +952,10 @@ div.itemDetailGalleryLink.defaultCardBackground { } } +.detailVerticalSection .emby-scrollbuttons { + padding-top: 0.4em; +} + .layout-tv .detailVerticalSection { margin-bottom: 3.4em !important; } @@ -954,6 +1044,10 @@ div.itemDetailGalleryLink.defaultCardBackground { margin-bottom: 2.7em; } +.layout-mobile .verticalSection-extrabottompadding { + margin-bottom: 1em; +} + .sectionTitleButton, .sectionTitleIconButton { margin-right: 0 !important; @@ -979,7 +1073,13 @@ div.itemDetailGalleryLink.defaultCardBackground { div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { margin: 0; - padding-top: 1.25em; + padding-top: 0.5em; + padding-bottom: 0.2em; +} + +.layout-mobile :not(.sectionTitleContainer-cards) > .sectionTitle-cards { + margin: 0; + padding-top: 0.5em; } .sectionTitleButton { @@ -1132,6 +1232,12 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { .trackSelections .selectContainer .selectLabel { margin: 0 0.2em 0 0; + line-height: 1.75; +} + +.layout-mobile .detailsGroupItem .label, +.layout-mobile .trackSelections .selectContainer .selectLabel { + flex-basis: 4.5em; } .trackSelections .selectContainer .detailTrackSelect { @@ -1144,3 +1250,21 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { margin-top: 0; font-size: 1.4em; } + +.overview-controls { + display: flex; + justify-content: flex-end; +} + +.detail-clamp-text { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 12; + -webkit-box-orient: vertical; +} + +@media all and (min-width: 40em) { + .detail-clamp-text { + -webkit-line-clamp: 6; + } +} diff --git a/src/assets/css/scrollstyles.css b/src/assets/css/scrollstyles.css index 1cb3207e06..67c6202252 100644 --- a/src/assets/css/scrollstyles.css +++ b/src/assets/css/scrollstyles.css @@ -12,6 +12,7 @@ .hiddenScrollX, .layout-tv .scrollX { -ms-overflow-style: none; + scrollbar-width: none; } .hiddenScrollX-forced { @@ -40,6 +41,7 @@ .hiddenScrollY, .layout-tv .smoothScrollY { -ms-overflow-style: none; + scrollbar-width: none; /* Can't do this because it not only hides the scrollbar, but also prevents scrolling */ diff --git a/src/assets/css/site.css b/src/assets/css/site.css index e59b639f45..38e056df89 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -5,9 +5,26 @@ html { height: 100%; } +.layout-mobile, +.layout-tv { + -webkit-touch-callout: none; + user-select: none; +} + +.clipForScreenReader { + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + .material-icons { /* Fix font ligatures on older WebOS versions */ - -webkit-font-feature-settings: "liga"; + font-feature-settings: "liga"; } .backgroundContainer { @@ -23,16 +40,6 @@ html { line-height: 1.35; } -.layout-mobile, -.layout-tv { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - body { overflow-x: hidden; background-color: transparent !important; @@ -109,3 +116,30 @@ div[data-role=page] { .headroom--unpinned { transform: translateY(-100%); } + +.drawerContent { + /* make sure the bottom of the drawer is visible when music is playing */ + padding-bottom: 4em; +} + +.force-scroll { + overflow-y: scroll; +} + +.hide-scroll { + overflow-y: hidden; +} + +.w-100 { + width: 100%; +} + +.margin-auto-x { + margin-left: auto; + margin-right: auto; +} + +.margin-auto-y { + margin-top: auto; + margin-bottom: auto; +} diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index f4f198325b..50cb41021b 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -30,7 +30,7 @@ opacity: 0; } -.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton) { +.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton):not(.headerSyncButton) { display: none; } diff --git a/src/assets/img/devices/edgechromium.svg b/src/assets/img/devices/edgechromium.svg new file mode 100644 index 0000000000..14d68a5d48 --- /dev/null +++ b/src/assets/img/devices/edgechromium.svg @@ -0,0 +1 @@ +Microsoft Edge icon diff --git a/src/bundle.js b/src/bundle.js index d7ba6c6a51..ae2a59f0d5 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -4,18 +4,18 @@ // Use define from require.js not webpack's define var _define = window.define; -// document-register-element -var docRegister = require('document-register-element'); -_define('document-register-element', function() { - return docRegister; -}); - // fetch var fetch = require('whatwg-fetch'); _define('fetch', function() { return fetch; }); +// Blurhash +var blurhash = require('blurhash'); +_define('blurhash', function() { + return blurhash; +}); + // query-string var query = require('query-string'); _define('queryString', function() { @@ -59,12 +59,6 @@ _define('resize-observer-polyfill', function() { return resize; }); -// shaka -var shaka = require('shaka-player'); -_define('shaka', function() { - return shaka; -}); - // swiper var swiper = require('swiper/js/swiper'); require('swiper/css/swiper.min.css'); @@ -102,6 +96,11 @@ _define('jellyfin-noto', function () { return noto; }); +var epubjs = require('epubjs'); +_define('epubjs', function () { + return epubjs; +}); + // page.js var page = require('page'); _define('page', function() { diff --git a/src/components/accessSchedule/accessSchedule.js b/src/components/accessSchedule/accessSchedule.js new file mode 100644 index 0000000000..b513766d0b --- /dev/null +++ b/src/components/accessSchedule/accessSchedule.js @@ -0,0 +1,97 @@ +/* eslint-disable indent */ + +/** + * Module for controlling user parental control from. + * @module components/accessSchedule/accessSchedule + */ + +import dialogHelper from 'dialogHelper'; +import datetime from 'datetime'; +import globalize from 'globalize'; +import 'emby-select'; +import 'paper-icon-button-light'; +import 'formDialogStyle'; + + function getDisplayTime(hours) { + let minutes = 0; + const pct = hours % 1; + + if (pct) { + minutes = parseInt(60 * pct); + } + + return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0)); + } + + function populateHours(context) { + let html = ''; + + for (let i = 0; i < 24; i++) { + html += ``; + } + + html += ``; + context.querySelector('#selectStart').innerHTML = html; + context.querySelector('#selectEnd').innerHTML = html; + } + + function loadSchedule(context, {DayOfWeek, StartHour, EndHour}) { + context.querySelector('#selectDay').value = DayOfWeek || 'Sunday'; + context.querySelector('#selectStart').value = StartHour || 0; + context.querySelector('#selectEnd').value = EndHour || 0; + } + + function submitSchedule(context, options) { + const updatedSchedule = { + DayOfWeek: context.querySelector('#selectDay').value, + StartHour: context.querySelector('#selectStart').value, + EndHour: context.querySelector('#selectEnd').value + }; + + if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) { + return void alert(globalize.translate('ErrorStartHourGreaterThanEnd')); + } + + context.submitted = true; + options.schedule = Object.assign(options.schedule, updatedSchedule); + dialogHelper.close(context); + } + + export function show(options) { + return new Promise((resolve, reject) => { + import('text!./accessSchedule.template.html').then(({default: template}) => { + const dlg = dialogHelper.createDialog({ + removeOnClose: true, + size: 'small' + }); + dlg.classList.add('formDialog'); + let html = ''; + html += globalize.translateHtml(template); + dlg.innerHTML = html; + populateHours(dlg); + loadSchedule(dlg, options.schedule); + dialogHelper.open(dlg); + dlg.addEventListener('close', () => { + if (dlg.submitted) { + resolve(options.schedule); + } else { + reject(); + } + }); + dlg.querySelector('.btnCancel').addEventListener('click', () => { + dialogHelper.close(dlg); + }); + dlg.querySelector('form').addEventListener('submit', event => { + submitSchedule(dlg, options); + event.preventDefault(); + return false; + }); + }); + }); + } + +/* eslint-enable indent */ + +export default { + show: show +}; diff --git a/src/components/accessschedule/accessschedule.template.html b/src/components/accessSchedule/accessSchedule.template.html similarity index 94% rename from src/components/accessschedule/accessschedule.template.html rename to src/components/accessSchedule/accessSchedule.template.html index 906d20869c..493150ae5e 100644 --- a/src/components/accessschedule/accessschedule.template.html +++ b/src/components/accessSchedule/accessSchedule.template.html @@ -1,6 +1,6 @@
-

${HeaderAccessSchedule} diff --git a/src/components/accessschedule/accessschedule.js b/src/components/accessschedule/accessschedule.js deleted file mode 100644 index 870231cf03..0000000000 --- a/src/components/accessschedule/accessschedule.js +++ /dev/null @@ -1,89 +0,0 @@ -define(['dialogHelper', 'datetime', 'globalize', 'emby-select', 'paper-icon-button-light', 'formDialogStyle'], function (dialogHelper, datetime, globalize) { - 'use strict'; - - function getDisplayTime(hours) { - var minutes = 0; - var pct = hours % 1; - - if (pct) { - minutes = parseInt(60 * pct); - } - - return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0)); - } - - function populateHours(context) { - var html = ''; - - for (var i = 0; i < 24; i++) { - html += ''; - } - - html += ''; - context.querySelector('#selectStart').innerHTML = html; - context.querySelector('#selectEnd').innerHTML = html; - } - - function loadSchedule(context, schedule) { - context.querySelector('#selectDay').value = schedule.DayOfWeek || 'Sunday'; - context.querySelector('#selectStart').value = schedule.StartHour || 0; - context.querySelector('#selectEnd').value = schedule.EndHour || 0; - } - - function submitSchedule(context, options) { - var updatedSchedule = { - DayOfWeek: context.querySelector('#selectDay').value, - StartHour: context.querySelector('#selectStart').value, - EndHour: context.querySelector('#selectEnd').value - }; - - if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) { - return void alert(globalize.translate('ErrorMessageStartHourGreaterThanEnd')); - } - - context.submitted = true; - options.schedule = Object.assign(options.schedule, updatedSchedule); - dialogHelper.close(context); - } - - return { - show: function (options) { - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'components/accessschedule/accessschedule.template.html', true); - - xhr.onload = function (e) { - var template = this.response; - var dlg = dialogHelper.createDialog({ - removeOnClose: true, - size: 'small' - }); - dlg.classList.add('formDialog'); - var html = ''; - html += globalize.translateDocument(template); - dlg.innerHTML = html; - populateHours(dlg); - loadSchedule(dlg, options.schedule); - dialogHelper.open(dlg); - dlg.addEventListener('close', function () { - if (dlg.submitted) { - resolve(options.schedule); - } else { - reject(); - } - }); - dlg.querySelector('.btnCancel').addEventListener('click', function (e) { - dialogHelper.close(dlg); - }); - dlg.querySelector('form').addEventListener('submit', function (e) { - submitSchedule(dlg, options); - e.preventDefault(); - return false; - }); - }; - - xhr.send(); - }); - } - }; -}); diff --git a/src/components/actionsheet/actionsheet.css b/src/components/actionSheet/actionSheet.css similarity index 100% rename from src/components/actionsheet/actionsheet.css rename to src/components/actionSheet/actionSheet.css diff --git a/src/components/actionSheet/actionSheet.js b/src/components/actionSheet/actionSheet.js new file mode 100644 index 0000000000..937cd2afe5 --- /dev/null +++ b/src/components/actionSheet/actionSheet.js @@ -0,0 +1,323 @@ +import dialogHelper from 'dialogHelper'; +import layoutManager from 'layoutManager'; +import globalize from 'globalize'; +import dom from 'dom'; +import 'emby-button'; +import 'css!./actionSheet'; +import 'material-icons'; +import 'scrollStyles'; +import 'listViewStyle'; + +function getOffsets(elems) { + let results = []; + + if (!document) { + return results; + } + + for (const elem of elems) { + let box = elem.getBoundingClientRect(); + + results.push({ + top: box.top, + left: box.left, + width: box.width, + height: box.height + }); + } + + return results; +} + +function getPosition(options, dlg) { + const windowSize = dom.getWindowSize(); + const windowHeight = windowSize.innerHeight; + const windowWidth = windowSize.innerWidth; + + let pos = getOffsets([options.positionTo])[0]; + + if (options.positionY !== 'top') { + pos.top += (pos.height || 0) / 2; + } + + pos.left += (pos.width || 0) / 2; + + const height = dlg.offsetHeight || 300; + const width = dlg.offsetWidth || 160; + + // Account for popup size + pos.top -= height / 2; + pos.left -= width / 2; + + // Avoid showing too close to the bottom + const overflowX = pos.left + width - windowWidth; + const overflowY = pos.top + height - windowHeight; + + if (overflowX > 0) { + pos.left -= (overflowX + 20); + } + if (overflowY > 0) { + pos.top -= (overflowY + 20); + } + + pos.top += (options.offsetTop || 0); + pos.left += (options.offsetLeft || 0); + + // Do some boundary checking + pos.top = Math.max(pos.top, 10); + pos.left = Math.max(pos.left, 10); + + return pos; +} + +function centerFocus(elem, horiz, on) { + import('scrollHelper').then(({default: scrollHelper}) => { + const fn = on ? 'on' : 'off'; + scrollHelper.centerFocus[fn](elem, horiz); + }); +} + +export function show(options) { + // items + // positionTo + // showCancel + // title + let dialogOptions = { + removeOnClose: true, + enableHistory: options.enableHistory, + scrollY: false + }; + + let isFullscreen; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; + isFullscreen = true; + dialogOptions.autoFocus = true; + } else { + dialogOptions.modal = false; + dialogOptions.entryAnimation = options.entryAnimation; + dialogOptions.exitAnimation = options.exitAnimation; + dialogOptions.entryAnimationDuration = options.entryAnimationDuration || 140; + dialogOptions.exitAnimationDuration = options.exitAnimationDuration || 100; + dialogOptions.autoFocus = false; + } + + let dlg = dialogHelper.createDialog(dialogOptions); + + if (isFullscreen) { + dlg.classList.add('actionsheet-fullscreen'); + } else { + dlg.classList.add('actionsheet-not-fullscreen'); + } + + dlg.classList.add('actionSheet'); + + if (options.dialogClass) { + dlg.classList.add(options.dialogClass); + } + + let html = ''; + + const scrollClassName = layoutManager.tv ? 'scrollY smoothScrollY hiddenScrollY' : 'scrollY'; + let style = ''; + + // Admittedly a hack but right now the scrollbar is being factored into the width which is causing truncation + if (options.items.length > 20) { + const minWidth = dom.getWindowSize().innerWidth >= 300 ? 240 : 200; + style += 'min-width:' + minWidth + 'px;'; + } + + let renderIcon = false; + let icons = []; + let itemIcon; + for (const item of options.items) { + itemIcon = item.icon || (item.selected ? 'check' : null); + + if (itemIcon) { + renderIcon = true; + } + icons.push(itemIcon || ''); + } + + if (layoutManager.tv) { + html += ``; + } + + // If any items have an icon, give them all an icon just to make sure they're all lined up evenly + const center = options.title && (!renderIcon /*|| itemsWithIcons.length != options.items.length*/); + + if (center || layoutManager.tv) { + html += '
'; + } else { + html += '
'; + } + + if (options.title) { + html += '

' + options.title + '

'; + } + if (options.text) { + html += '

' + options.text + '

'; + } + + let scrollerClassName = 'actionSheetScroller'; + if (layoutManager.tv) { + scrollerClassName += ' actionSheetScroller-tv focuscontainer-x focuscontainer-y'; + } + html += '
'; + + let menuItemClass = 'listItem listItem-button actionSheetMenuItem'; + + if (options.border || options.shaded) { + menuItemClass += ' listItem-border'; + } + + if (options.menuItemClass) { + menuItemClass += ' ' + options.menuItemClass; + } + + if (layoutManager.tv) { + menuItemClass += ' listItem-focusscale'; + } + + if (layoutManager.mobile) { + menuItemClass += ' actionsheet-xlargeFont'; + } + + // 'options.items' is HTMLOptionsCollection, so no fancy loops + for (let i = 0; i < options.items.length; i++) { + const item = options.items[i]; + + if (item.divider) { + html += '
'; + continue; + } + + const autoFocus = item.selected && layoutManager.tv ? ' autoFocus' : ''; + + // Check for null in case int 0 was passed in + const optionId = item.id == null || item.id === '' ? item.value : item.id; + html += ''; + + itemIcon = icons[i]; + + if (itemIcon) { + html += ``; + } else if (renderIcon && !center) { + html += ''; + } + + html += '
'; + + html += '
'; + html += (item.name || item.textContent || item.innerText); + html += '
'; + + if (item.secondaryText) { + html += `
${item.secondaryText}
`; + } + + html += '
'; + + if (item.asideText) { + html += `
${item.asideText}
`; + } + + html += ''; + } + + if (options.showCancel) { + html += '
'; + html += ``; + html += '
'; + } + html += '
'; + + dlg.innerHTML = html; + + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.actionSheetScroller'), false, true); + } + + let btnCloseActionSheet = dlg.querySelector('.btnCloseActionSheet'); + if (btnCloseActionSheet) { + btnCloseActionSheet.addEventListener('click', function () { + dialogHelper.close(dlg); + }); + } + + let selectedId; + + let timeout; + if (options.timeout) { + timeout = setTimeout(function () { + dialogHelper.close(dlg); + }, options.timeout); + } + + return new Promise(function (resolve, reject) { + let isResolved; + + dlg.addEventListener('click', function (e) { + const actionSheetMenuItem = dom.parentWithClass(e.target, 'actionSheetMenuItem'); + + if (actionSheetMenuItem) { + selectedId = actionSheetMenuItem.getAttribute('data-id'); + + if (options.resolveOnClick) { + if (options.resolveOnClick.indexOf) { + if (options.resolveOnClick.indexOf(selectedId) !== -1) { + resolve(selectedId); + isResolved = true; + } + } else { + resolve(selectedId); + isResolved = true; + } + } + + dialogHelper.close(dlg); + } + }); + + dlg.addEventListener('close', function () { + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.actionSheetScroller'), false, false); + } + + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + if (!isResolved) { + if (selectedId != null) { + if (options.callback) { + options.callback(selectedId); + } + + resolve(selectedId); + } else { + reject(); + } + } + }); + + dialogHelper.open(dlg); + + const pos = options.positionTo && dialogOptions.size !== 'fullscreen' ? getPosition(options, dlg) : null; + + if (pos) { + dlg.style.position = 'fixed'; + dlg.style.margin = 0; + dlg.style.left = pos.left + 'px'; + dlg.style.top = pos.top + 'px'; + } + }); +} + +export default { + show: show +}; diff --git a/src/components/actionsheet/actionsheet.js b/src/components/actionsheet/actionsheet.js deleted file mode 100644 index e08fbf4a25..0000000000 --- a/src/components/actionsheet/actionsheet.js +++ /dev/null @@ -1,360 +0,0 @@ -define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-button', 'css!./actionsheet', 'material-icons', 'scrollStyles', 'listViewStyle'], function (dialogHelper, layoutManager, globalize, browser, dom) { - 'use strict'; - - function getOffsets(elems) { - - var doc = document; - var results = []; - - if (!doc) { - return results; - } - - var box; - var elem; - - for (var i = 0, length = elems.length; i < length; i++) { - - elem = elems[i]; - // Support: BlackBerry 5, iOS 3 (original iPhone) - // If we don't have gBCR, just use 0,0 rather than error - if (elem.getBoundingClientRect) { - box = elem.getBoundingClientRect(); - } else { - box = { top: 0, left: 0 }; - } - - results[i] = { - top: box.top, - left: box.left, - width: box.width, - height: box.height - }; - } - - return results; - } - - function getPosition(options, dlg) { - - var windowSize = dom.getWindowSize(); - var windowHeight = windowSize.innerHeight; - var windowWidth = windowSize.innerWidth; - - var pos = getOffsets([options.positionTo])[0]; - - if (options.positionY !== 'top') { - pos.top += (pos.height || 0) / 2; - } - - pos.left += (pos.width || 0) / 2; - - var height = dlg.offsetHeight || 300; - var width = dlg.offsetWidth || 160; - - // Account for popup size - pos.top -= height / 2; - pos.left -= width / 2; - - // Avoid showing too close to the bottom - var overflowX = pos.left + width - windowWidth; - var overflowY = pos.top + height - windowHeight; - - if (overflowX > 0) { - pos.left -= (overflowX + 20); - } - if (overflowY > 0) { - pos.top -= (overflowY + 20); - } - - pos.top += (options.offsetTop || 0); - pos.left += (options.offsetLeft || 0); - - // Do some boundary checking - pos.top = Math.max(pos.top, 10); - pos.left = Math.max(pos.left, 10); - - return pos; - } - - function centerFocus(elem, horiz, on) { - require(['scrollHelper'], function (scrollHelper) { - var fn = on ? 'on' : 'off'; - scrollHelper.centerFocus[fn](elem, horiz); - }); - } - - function show(options) { - - // items - // positionTo - // showCancel - // title - var dialogOptions = { - removeOnClose: true, - enableHistory: options.enableHistory, - scrollY: false - }; - - var backButton = false; - var isFullscreen; - - if (layoutManager.tv) { - dialogOptions.size = 'fullscreen'; - isFullscreen = true; - backButton = true; - dialogOptions.autoFocus = true; - } else { - - dialogOptions.modal = false; - dialogOptions.entryAnimation = options.entryAnimation; - dialogOptions.exitAnimation = options.exitAnimation; - dialogOptions.entryAnimationDuration = options.entryAnimationDuration || 140; - dialogOptions.exitAnimationDuration = options.exitAnimationDuration || 100; - dialogOptions.autoFocus = false; - } - - var dlg = dialogHelper.createDialog(dialogOptions); - - if (isFullscreen) { - dlg.classList.add('actionsheet-fullscreen'); - } else { - dlg.classList.add('actionsheet-not-fullscreen'); - } - - dlg.classList.add('actionSheet'); - - if (options.dialogClass) { - dlg.classList.add(options.dialogClass); - } - - var html = ''; - - var scrollClassName = layoutManager.tv ? 'scrollY smoothScrollY hiddenScrollY' : 'scrollY'; - var style = ''; - - // Admittedly a hack but right now the scrollbar is being factored into the width which is causing truncation - if (options.items.length > 20) { - var minWidth = dom.getWindowSize().innerWidth >= 300 ? 240 : 200; - style += 'min-width:' + minWidth + 'px;'; - } - - var i; - var length; - var option; - var renderIcon = false; - var icons = []; - var itemIcon; - for (i = 0, length = options.items.length; i < length; i++) { - - option = options.items[i]; - - itemIcon = option.icon || (option.selected ? 'check' : null); - - if (itemIcon) { - renderIcon = true; - } - icons.push(itemIcon || ''); - } - - if (layoutManager.tv) { - html += ''; - } - - // If any items have an icon, give them all an icon just to make sure they're all lined up evenly - var center = options.title && (!renderIcon /*|| itemsWithIcons.length != options.items.length*/); - - if (center || layoutManager.tv) { - html += '
'; - } else { - html += '
'; - } - - if (options.title) { - - html += '

'; - html += options.title; - html += '

'; - } - if (options.text) { - html += '

'; - html += options.text; - html += '

'; - } - - var scrollerClassName = 'actionSheetScroller'; - if (layoutManager.tv) { - scrollerClassName += ' actionSheetScroller-tv focuscontainer-x focuscontainer-y'; - } - html += '
'; - - var menuItemClass = 'listItem listItem-button actionSheetMenuItem'; - - if (options.border || options.shaded) { - menuItemClass += ' listItem-border'; - } - - if (options.menuItemClass) { - menuItemClass += ' ' + options.menuItemClass; - } - - if (layoutManager.tv) { - menuItemClass += ' listItem-focusscale'; - } - - if (layoutManager.mobile) { - menuItemClass += ' actionsheet-xlargeFont'; - } - - for (i = 0, length = options.items.length; i < length; i++) { - - option = options.items[i]; - - if (option.divider) { - - html += '
'; - continue; - } - - var autoFocus = option.selected && layoutManager.tv ? ' autoFocus' : ''; - - // Check for null in case int 0 was passed in - var optionId = option.id == null || option.id === '' ? option.value : option.id; - html += ''; - - itemIcon = icons[i]; - - if (itemIcon) { - - html += ''; - } else if (renderIcon && !center) { - html += ''; - } - - html += '
'; - - html += '
'; - html += (option.name || option.textContent || option.innerText); - html += '
'; - - if (option.secondaryText) { - html += '
'; - html += option.secondaryText; - html += '
'; - } - - html += '
'; - - if (option.asideText) { - html += '
'; - html += option.asideText; - html += '
'; - } - - html += ''; - } - - if (options.showCancel) { - html += '
'; - html += ''; - html += '
'; - } - html += '
'; - - dlg.innerHTML = html; - - if (layoutManager.tv) { - centerFocus(dlg.querySelector('.actionSheetScroller'), false, true); - } - - var btnCloseActionSheet = dlg.querySelector('.btnCloseActionSheet'); - if (btnCloseActionSheet) { - dlg.querySelector('.btnCloseActionSheet').addEventListener('click', function () { - dialogHelper.close(dlg); - }); - } - - // Seeing an issue in some non-chrome browsers where this is requiring a double click - //var eventName = browser.firefox ? 'mousedown' : 'click'; - var selectedId; - - var timeout; - if (options.timeout) { - timeout = setTimeout(function () { - dialogHelper.close(dlg); - }, options.timeout); - } - - return new Promise(function (resolve, reject) { - - var isResolved; - - dlg.addEventListener('click', function (e) { - - var actionSheetMenuItem = dom.parentWithClass(e.target, 'actionSheetMenuItem'); - - if (actionSheetMenuItem) { - selectedId = actionSheetMenuItem.getAttribute('data-id'); - - if (options.resolveOnClick) { - - if (options.resolveOnClick.indexOf) { - - if (options.resolveOnClick.indexOf(selectedId) !== -1) { - - resolve(selectedId); - isResolved = true; - } - - } else { - resolve(selectedId); - isResolved = true; - } - } - - dialogHelper.close(dlg); - } - - }); - - dlg.addEventListener('close', function () { - - if (layoutManager.tv) { - centerFocus(dlg.querySelector('.actionSheetScroller'), false, false); - } - - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - - if (!isResolved) { - if (selectedId != null) { - if (options.callback) { - options.callback(selectedId); - } - - resolve(selectedId); - } else { - reject(); - } - } - }); - - dialogHelper.open(dlg); - - var pos = options.positionTo && dialogOptions.size !== 'fullscreen' ? getPosition(options, dlg) : null; - - if (pos) { - dlg.style.position = 'fixed'; - dlg.style.margin = 0; - dlg.style.left = pos.left + 'px'; - dlg.style.top = pos.top + 'px'; - } - }); - } - - return { - show: show - }; -}); diff --git a/src/components/activitylog.js b/src/components/activitylog.js index a7b3f48bc2..ab489a3f31 100644 --- a/src/components/activitylog.js +++ b/src/components/activitylog.js @@ -1,13 +1,22 @@ -define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', 'serverNotifications', 'connectionManager', 'emby-button', 'listViewStyle'], function (events, globalize, dom, datefns, dfnshelper, userSettings, serverNotifications, connectionManager) { - 'use strict'; +import events from 'events'; +import globalize from 'globalize'; +import dom from 'dom'; +import * as datefns from 'date-fns'; +import dfnshelper from 'dfnshelper'; +import serverNotifications from 'serverNotifications'; +import connectionManager from 'connectionManager'; +import 'emby-button'; +import 'listViewStyle'; + +/* eslint-disable indent */ function getEntryHtml(entry, apiClient) { - var html = ''; + let html = ''; html += '
'; - var color = '#00a4dc'; - var icon = 'notifications'; + let color = '#00a4dc'; + let icon = 'notifications'; - if ('Error' == entry.Severity || 'Fatal' == entry.Severity || 'Warn' == entry.Severity) { + if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') { color = '#cc0000'; icon = 'notification_important'; } @@ -34,10 +43,14 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', html += '
'; if (entry.Overview) { - html += ''; + html += ``; } - return html += '
'; + html += '
'; + + return html; } function renderList(elem, apiClient, result, startIndex, limit) { @@ -47,14 +60,15 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', } function reloadData(instance, elem, apiClient, startIndex, limit) { - if (null == startIndex) { + if (startIndex == null) { startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0'); } limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7'); - var minDate = new Date(); - var hasUserId = 'false' !== elem.getAttribute('data-useractivity'); + const minDate = new Date(); + const hasUserId = elem.getAttribute('data-useractivity') !== 'false'; + // TODO: Use date-fns if (hasUserId) { minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back } else { @@ -70,7 +84,7 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', elem.setAttribute('data-activitystartindex', startIndex); elem.setAttribute('data-activitylimit', limit); if (!startIndex) { - var activityContainer = dom.parentWithClass(elem, 'activityContainer'); + const activityContainer = dom.parentWithClass(elem, 'activityContainer'); if (activityContainer) { if (result.Items.length) { @@ -87,7 +101,7 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', } function onActivityLogUpdate(e, apiClient, data) { - var options = this.options; + const options = this.options; if (options && options.serverId === apiClient.serverId()) { reloadData(this, options.element, apiClient); @@ -95,14 +109,14 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', } function onListClick(e) { - var btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo'); + const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo'); if (btnEntryInfo) { - var id = btnEntryInfo.getAttribute('data-id'); - var items = this.items; + const id = btnEntryInfo.getAttribute('data-id'); + const items = this.items; if (items) { - var item = items.filter(function (i) { + const item = items.filter(function (i) { return i.Id.toString() === id; })[0]; @@ -114,35 +128,35 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', } function showItemOverview(item) { - require(['alert'], function (alert) { + import('alert').then(({default: alert}) => { alert({ text: item.Overview }); }); } - function ActivityLog(options) { +class ActivityLog { + constructor(options) { this.options = options; - var element = options.element; + const element = options.element; element.classList.add('activityLogListWidget'); element.addEventListener('click', onListClick.bind(this)); - var apiClient = connectionManager.getApiClient(options.serverId); + const apiClient = connectionManager.getApiClient(options.serverId); reloadData(this, element, apiClient); - var onUpdate = onActivityLogUpdate.bind(this); + const onUpdate = onActivityLogUpdate.bind(this); this.updateFn = onUpdate; events.on(serverNotifications, 'ActivityLogEntry', onUpdate); apiClient.sendMessage('ActivityLogEntryStart', '0,1500'); } - - ActivityLog.prototype.destroy = function () { - var options = this.options; + destroy() { + const options = this.options; if (options) { options.element.classList.remove('activityLogListWidget'); connectionManager.getApiClient(options.serverId).sendMessage('ActivityLogEntryStop', '0,1500'); } - var onUpdate = this.updateFn; + const onUpdate = this.updateFn; if (onUpdate) { events.off(serverNotifications, 'ActivityLogEntry', onUpdate); @@ -150,7 +164,9 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', this.items = null; this.options = null; - }; + } +} - return ActivityLog; -}); +export default ActivityLog; + +/* eslint-enable indent */ diff --git a/src/components/alert.js b/src/components/alert.js index 8a37ac1845..1420c7f428 100644 --- a/src/components/alert.js +++ b/src/components/alert.js @@ -1,14 +1,16 @@ -define(['browser', 'dialog', 'globalize'], function (browser, dialog, globalize) { - 'use strict'; +import browser from 'browser'; +import dialog from 'dialog'; +import globalize from 'globalize'; + +/* eslint-disable indent */ function replaceAll(originalString, strReplace, strWith) { - var reg = new RegExp(strReplace, 'ig'); + const reg = new RegExp(strReplace, 'ig'); return originalString.replace(reg, strWith); } - return function (text, title) { - - var options; + export default function (text, title) { + let options; if (typeof text === 'string') { options = { title: title, @@ -21,7 +23,7 @@ define(['browser', 'dialog', 'globalize'], function (browser, dialog, globalize) if (browser.tv && window.alert) { alert(replaceAll(options.text || '', '
', '\n')); } else { - var items = []; + const items = []; items.push({ name: globalize.translate('ButtonGotIt'), @@ -31,7 +33,7 @@ define(['browser', 'dialog', 'globalize'], function (browser, dialog, globalize) options.buttons = items; - return dialog(options).then(function (result) { + return dialog.show(options).then(function (result) { if (result === 'ok') { return Promise.resolve(); } @@ -41,5 +43,6 @@ define(['browser', 'dialog', 'globalize'], function (browser, dialog, globalize) } return Promise.resolve(); - }; -}); + } + +/* eslint-enable indent */ diff --git a/src/components/alphaPicker/alphaPicker.js b/src/components/alphaPicker/alphaPicker.js new file mode 100644 index 0000000000..95b5881677 --- /dev/null +++ b/src/components/alphaPicker/alphaPicker.js @@ -0,0 +1,313 @@ +/* eslint-disable indent */ + +/** + * Module alphaPicker. + * @module components/alphaPicker/alphaPicker + */ + +import focusManager from 'focusManager'; +import layoutManager from 'layoutManager'; +import dom from 'dom'; +import 'css!./style.css'; +import 'paper-icon-button-light'; +import 'material-icons'; + + const selectedButtonClass = 'alphaPickerButton-selected'; + + function focus() { + const scope = this; + const selected = scope.querySelector(`.${selectedButtonClass}`); + + if (selected) { + focusManager.focus(selected); + } else { + focusManager.autoFocus(scope, true); + } + } + + function getAlphaPickerButtonClassName(vertical) { + let alphaPickerButtonClassName = 'alphaPickerButton'; + + if (layoutManager.tv) { + alphaPickerButtonClassName += ' alphaPickerButton-tv'; + } + + if (vertical) { + alphaPickerButtonClassName += ' alphaPickerButton-vertical'; + } + + return alphaPickerButtonClassName; + } + + function getLetterButton(l, vertical) { + return ``; + } + + function mapLetters(letters, vertical) { + return letters.map(l => { + return getLetterButton(l, vertical); + }); + } + + function render(element, options) { + element.classList.add('alphaPicker'); + + if (layoutManager.tv) { + element.classList.add('alphaPicker-tv'); + } + + const vertical = element.classList.contains('alphaPicker-vertical'); + + if (!vertical) { + element.classList.add('focuscontainer-x'); + } + + let html = ''; + let letters; + + const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical); + + let rowClassName = 'alphaPickerRow'; + + if (vertical) { + rowClassName += ' alphaPickerRow-vertical'; + } + + html += `
`; + if (options.mode === 'keyboard') { + html += ``; + } else { + letters = ['#']; + html += mapLetters(letters, vertical).join(''); + } + + letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; + html += mapLetters(letters, vertical).join(''); + + if (options.mode === 'keyboard') { + html += ``; + html += '
'; + + letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + html += `
`; + html += '
'; + html += mapLetters(letters, vertical).join(''); + html += '
'; + } else { + html += '
'; + } + + element.innerHTML = html; + + element.classList.add('focusable'); + element.focus = focus; + } + + export class AlphaPicker { + constructor(options) { + const self = this; + + this.options = options; + + const element = options.element; + const itemsContainer = options.itemsContainer; + const itemClass = options.itemClass; + + let itemFocusValue; + let itemFocusTimeout; + + function onItemFocusTimeout() { + itemFocusTimeout = null; + self.value(itemFocusValue); + } + + let alphaFocusedElement; + let alphaFocusTimeout; + + function onAlphaFocusTimeout() { + alphaFocusTimeout = null; + + if (document.activeElement === alphaFocusedElement) { + const value = alphaFocusedElement.getAttribute('data-value'); + self.value(value, true); + } + } + + function onAlphaPickerInKeyboardModeClick(e) { + const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); + + if (alphaPickerButton) { + const value = alphaPickerButton.getAttribute('data-value'); + + element.dispatchEvent(new CustomEvent('alphavalueclicked', { + cancelable: false, + detail: { + value + } + })); + } + } + + function onAlphaPickerClick(e) { + const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); + + if (alphaPickerButton) { + const value = alphaPickerButton.getAttribute('data-value'); + if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) { + this.value(null, true); + } else { + this.value(value, true); + } + } + } + + function onAlphaPickerFocusIn(e) { + if (alphaFocusTimeout) { + clearTimeout(alphaFocusTimeout); + alphaFocusTimeout = null; + } + + const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); + + if (alphaPickerButton) { + alphaFocusedElement = alphaPickerButton; + alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600); + } + } + + function onItemsFocusIn(e) { + const item = dom.parentWithClass(e.target, itemClass); + + if (item) { + const prefix = item.getAttribute('data-prefix'); + if (prefix && prefix.length) { + itemFocusValue = prefix[0]; + if (itemFocusTimeout) { + clearTimeout(itemFocusTimeout); + } + itemFocusTimeout = setTimeout(onItemFocusTimeout, 100); + } + } + } + + this.enabled = function (enabled) { + if (enabled) { + if (itemsContainer) { + itemsContainer.addEventListener('focus', onItemsFocusIn, true); + } + + if (options.mode === 'keyboard') { + element.addEventListener('click', onAlphaPickerInKeyboardModeClick); + } + + if (options.valueChangeEvent !== 'click') { + element.addEventListener('focus', onAlphaPickerFocusIn, true); + } else { + element.addEventListener('click', onAlphaPickerClick.bind(this)); + } + } else { + if (itemsContainer) { + itemsContainer.removeEventListener('focus', onItemsFocusIn, true); + } + + element.removeEventListener('click', onAlphaPickerInKeyboardModeClick); + element.removeEventListener('focus', onAlphaPickerFocusIn, true); + element.removeEventListener('click', onAlphaPickerClick.bind(this)); + } + }; + + render(element, options); + + this.enabled(true); + this.visible(true); + } + + value(value, applyValue) { + const element = this.options.element; + let btn; + let selected; + + if (value !== undefined) { + if (value != null) { + value = value.toUpperCase(); + this._currentValue = value; + + if (this.options.mode !== 'keyboard') { + selected = element.querySelector(`.${selectedButtonClass}`); + + try { + btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`); + } catch (err) { + console.error('error in querySelector:', err); + } + + if (btn && btn !== selected) { + btn.classList.add(selectedButtonClass); + } + if (selected && selected !== btn) { + selected.classList.remove(selectedButtonClass); + } + } + } else { + this._currentValue = value; + + selected = element.querySelector(`.${selectedButtonClass}`); + if (selected) { + selected.classList.remove(selectedButtonClass); + } + } + } + + if (applyValue) { + element.dispatchEvent(new CustomEvent('alphavaluechanged', { + cancelable: false, + detail: { + value + } + })); + } + + return this._currentValue; + } + + on(name, fn) { + const element = this.options.element; + element.addEventListener(name, fn); + } + + off(name, fn) { + const element = this.options.element; + element.removeEventListener(name, fn); + } + + visible(visible) { + const element = this.options.element; + element.style.visibility = visible ? 'visible' : 'hidden'; + } + + values() { + const element = this.options.element; + const elems = element.querySelectorAll('.alphaPickerButton'); + const values = []; + for (let i = 0, length = elems.length; i < length; i++) { + values.push(elems[i].getAttribute('data-value')); + } + + return values; + } + + focus() { + const element = this.options.element; + focusManager.autoFocus(element, true); + } + + destroy() { + const element = this.options.element; + this.enabled(false); + element.classList.remove('focuscontainer-x'); + this.options = null; + } + } + +/* eslint-enable indent */ +export default AlphaPicker; diff --git a/src/components/alphapicker/style.css b/src/components/alphaPicker/style.css similarity index 100% rename from src/components/alphapicker/style.css rename to src/components/alphaPicker/style.css diff --git a/src/components/alphapicker/alphapicker.js b/src/components/alphapicker/alphapicker.js deleted file mode 100644 index 79f74879e5..0000000000 --- a/src/components/alphapicker/alphapicker.js +++ /dev/null @@ -1,321 +0,0 @@ -define(['focusManager', 'layoutManager', 'dom', 'css!./style.css', 'paper-icon-button-light', 'material-icons'], function (focusManager, layoutManager, dom) { - 'use strict'; - - var selectedButtonClass = 'alphaPickerButton-selected'; - - function focus() { - var scope = this; - var selected = scope.querySelector('.' + selectedButtonClass); - - if (selected) { - focusManager.focus(selected); - } else { - focusManager.autoFocus(scope, true); - } - } - - function getAlphaPickerButtonClassName(vertical) { - - var alphaPickerButtonClassName = 'alphaPickerButton'; - - if (layoutManager.tv) { - alphaPickerButtonClassName += ' alphaPickerButton-tv'; - } - - if (vertical) { - alphaPickerButtonClassName += ' alphaPickerButton-vertical'; - } - - return alphaPickerButtonClassName; - } - - function getLetterButton(l, vertical) { - return ''; - } - - function mapLetters(letters, vertical) { - - return letters.map(function (l) { - return getLetterButton(l, vertical); - }); - } - - function render(element, options) { - - element.classList.add('alphaPicker'); - - if (layoutManager.tv) { - element.classList.add('alphaPicker-tv'); - } - - var vertical = element.classList.contains('alphaPicker-vertical'); - - if (!vertical) { - element.classList.add('focuscontainer-x'); - } - - var html = ''; - var letters; - - var alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical); - - var rowClassName = 'alphaPickerRow'; - - if (vertical) { - rowClassName += ' alphaPickerRow-vertical'; - } - - html += '
'; - if (options.mode === 'keyboard') { - html += ''; - } else { - letters = ['#']; - html += mapLetters(letters, vertical).join(''); - } - - letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; - html += mapLetters(letters, vertical).join(''); - - if (options.mode === 'keyboard') { - html += ''; - html += '
'; - - letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - html += '
'; - html += '
'; - html += mapLetters(letters, vertical).join(''); - html += '
'; - } else { - html += '
'; - } - - element.innerHTML = html; - - element.classList.add('focusable'); - element.focus = focus; - } - - function AlphaPicker(options) { - - var self = this; - this.options = options; - - var element = options.element; - var itemsContainer = options.itemsContainer; - var itemClass = options.itemClass; - - var itemFocusValue; - var itemFocusTimeout; - - function onItemFocusTimeout() { - itemFocusTimeout = null; - self.value(itemFocusValue); - } - - var alphaFocusedElement; - var alphaFocusTimeout; - - function onAlphaFocusTimeout() { - - alphaFocusTimeout = null; - - if (document.activeElement === alphaFocusedElement) { - var value = alphaFocusedElement.getAttribute('data-value'); - self.value(value, true); - } - } - - function onAlphaPickerInKeyboardModeClick(e) { - - var alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); - - if (alphaPickerButton) { - var value = alphaPickerButton.getAttribute('data-value'); - - element.dispatchEvent(new CustomEvent('alphavalueclicked', { - cancelable: false, - detail: { - value: value - } - })); - } - } - - function onAlphaPickerClick(e) { - - var alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); - - if (alphaPickerButton) { - var value = alphaPickerButton.getAttribute('data-value'); - if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) { - self.value(null, true); - } else { - self.value(value, true); - } - } - } - - function onAlphaPickerFocusIn(e) { - - if (alphaFocusTimeout) { - clearTimeout(alphaFocusTimeout); - alphaFocusTimeout = null; - } - - var alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton'); - - if (alphaPickerButton) { - alphaFocusedElement = alphaPickerButton; - alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600); - } - } - - function onItemsFocusIn(e) { - - var item = dom.parentWithClass(e.target, itemClass); - - if (item) { - var prefix = item.getAttribute('data-prefix'); - if (prefix && prefix.length) { - - itemFocusValue = prefix[0]; - if (itemFocusTimeout) { - clearTimeout(itemFocusTimeout); - } - itemFocusTimeout = setTimeout(onItemFocusTimeout, 100); - } - } - } - - self.enabled = function (enabled) { - - if (enabled) { - - if (itemsContainer) { - itemsContainer.addEventListener('focus', onItemsFocusIn, true); - } - - if (options.mode === 'keyboard') { - element.addEventListener('click', onAlphaPickerInKeyboardModeClick); - } - - if (options.valueChangeEvent !== 'click') { - element.addEventListener('focus', onAlphaPickerFocusIn, true); - } else { - element.addEventListener('click', onAlphaPickerClick.bind(this)); - } - - } else { - - if (itemsContainer) { - itemsContainer.removeEventListener('focus', onItemsFocusIn, true); - } - - element.removeEventListener('click', onAlphaPickerInKeyboardModeClick); - element.removeEventListener('focus', onAlphaPickerFocusIn, true); - element.removeEventListener('click', onAlphaPickerClick.bind(this)); - } - }; - - render(element, options); - - this.enabled(true); - this.visible(true); - } - - AlphaPicker.prototype.value = function (value, applyValue) { - - var element = this.options.element; - var btn; - var selected; - - if (value !== undefined) { - if (value != null) { - - value = value.toUpperCase(); - this._currentValue = value; - - if (this.options.mode !== 'keyboard') { - selected = element.querySelector('.' + selectedButtonClass); - - try { - btn = element.querySelector('.alphaPickerButton[data-value=\'' + value + '\']'); - } catch (err) { - console.error('error in querySelector: ' + err); - } - - if (btn && btn !== selected) { - btn.classList.add(selectedButtonClass); - } - if (selected && selected !== btn) { - selected.classList.remove(selectedButtonClass); - } - } - } else { - this._currentValue = value; - - selected = element.querySelector('.' + selectedButtonClass); - if (selected) { - selected.classList.remove(selectedButtonClass); - } - } - } - - if (applyValue) { - element.dispatchEvent(new CustomEvent('alphavaluechanged', { - cancelable: false, - detail: { - value: value - } - })); - } - - return this._currentValue; - }; - - AlphaPicker.prototype.on = function (name, fn) { - var element = this.options.element; - element.addEventListener(name, fn); - }; - - AlphaPicker.prototype.off = function (name, fn) { - var element = this.options.element; - element.removeEventListener(name, fn); - }; - - AlphaPicker.prototype.visible = function (visible) { - - var element = this.options.element; - element.style.visibility = visible ? 'visible' : 'hidden'; - }; - - AlphaPicker.prototype.values = function () { - - var element = this.options.element; - var elems = element.querySelectorAll('.alphaPickerButton'); - var values = []; - for (var i = 0, length = elems.length; i < length; i++) { - - values.push(elems[i].getAttribute('data-value')); - - } - - return values; - }; - - AlphaPicker.prototype.focus = function () { - - var element = this.options.element; - focusManager.autoFocus(element, true); - }; - - AlphaPicker.prototype.destroy = function () { - - var element = this.options.element; - this.enabled(false); - element.classList.remove('focuscontainer-x'); - this.options = null; - }; - - return AlphaPicker; -}); diff --git a/src/components/appfooter/appfooter.css b/src/components/appFooter/appFooter.css similarity index 100% rename from src/components/appfooter/appfooter.css rename to src/components/appFooter/appFooter.css diff --git a/src/components/appfooter/appfooter.js b/src/components/appFooter/appFooter.js similarity index 54% rename from src/components/appfooter/appfooter.js rename to src/components/appFooter/appFooter.js index 07d7701ff2..c60aa1a27c 100644 --- a/src/components/appfooter/appfooter.js +++ b/src/components/appFooter/appFooter.js @@ -1,17 +1,17 @@ -define(['browser', 'css!./appfooter'], function (browser) { - 'use strict'; +import 'css!./appFooter'; - function render(options) { - var elem = document.createElement('div'); - elem.classList.add('appfooter'); +function render(options) { + const elem = document.createElement('div'); + elem.classList.add('appfooter'); - document.body.appendChild(elem); + document.body.appendChild(elem); - return elem; - } + return elem; +} - function appFooter(options) { - var self = this; +class appFooter { + constructor(options) { + const self = this; self.element = render(options); self.add = function (elem) { @@ -26,12 +26,11 @@ define(['browser', 'css!./appfooter'], function (browser) { } }; } - - appFooter.prototype.destroy = function () { + destroy() { var self = this; self.element = null; - }; + } +} - return appFooter; -}); +export default appFooter; diff --git a/src/components/appRouter.js b/src/components/appRouter.js index 2e11ef88d9..da3b08317c 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -1,6 +1,9 @@ -define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinManager', 'pluginManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, layoutManager, skinManager, pluginManager, backdrop, browser, page, appSettings, appHost, connectionManager) { +define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, skinManager, backdrop, browser, page, appSettings, appHost, connectionManager) { 'use strict'; + browser = browser.default || browser; + loading = loading.default || loading; + var appRouter = { showLocalLogin: function (serverId, manualLogin) { var pageName = manualLogin ? 'manuallogin' : 'login'; @@ -16,25 +19,25 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM show('/settings/settings.html'); }, showNowPlaying: function () { - show('/nowplaying.html'); + show('queue'); } }; function beginConnectionWizard() { - backdrop.clear(); + backdrop.clearBackdrop(); loading.show(); connectionManager.connect({ enableAutoLogin: appSettings.enableAutoLogin() }).then(function (result) { - handleConnectionResult(result, loading); + handleConnectionResult(result); }); } - function handleConnectionResult(result, loading) { + function handleConnectionResult(result) { switch (result.State) { case 'SignedIn': loading.hide(); - skinManager.loadUserSkin(); + Emby.Page.goHome(); break; case 'ServerSignIn': result.ApiClient.getPublicUsers().then(function (users) { @@ -53,7 +56,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM break; case 'ServerUpdateNeeded': require(['alert'], function (alert) { - alert({ + alert.default({ text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'), html: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin') }).then(function () { @@ -147,26 +150,19 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM if (typeof route.path === 'string') { loadContentUrl(ctx, next, route, currentRequest); } else { - // ? TODO next(); } }; if (!isBackNav) { - // Don't force a new view for home due to the back menu - //if (route.type !== 'home') { onNewViewNeeded(); return; - //} } viewManager.tryRestoreView(currentRequest, function () { - - // done currentRouteInfo = { route: route, path: ctx.path }; - }).catch(function (result) { if (!result || !result.cancelled) { onNewViewNeeded(); @@ -197,12 +193,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function onRequestFail(e, data) { - var apiClient = this; if (data.status === 403) { if (data.errorCode === 'ParentalControl') { - var isCurrentAllowed = currentRouteInfo ? (currentRouteInfo.route.anonymous || currentRouteInfo.route.startup) : true; // Bounce to the login screen, but not if a password entry fails, obviously @@ -210,7 +204,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM showForcedLogoutMessage(globalize.translate('AccessRestrictedTryAgainLater')); appRouter.showLocalLogin(apiClient.serverId()); } - } } } @@ -222,48 +215,13 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function normalizeImageOptions(options) { - var scaleFactor = browser.tv ? 0.8 : 1; - var setQuality; - if (options.maxWidth) { - options.maxWidth = Math.round(options.maxWidth * scaleFactor); + if (options.maxWidth || options.width || options.maxHeight || options.height) { setQuality = true; } - if (options.width) { - options.width = Math.round(options.width * scaleFactor); - setQuality = true; - } - - if (options.maxHeight) { - options.maxHeight = Math.round(options.maxHeight * scaleFactor); - setQuality = true; - } - - if (options.height) { - options.height = Math.round(options.height * scaleFactor); - setQuality = true; - } - - if (setQuality) { - - var quality = 100; - - var type = options.type || 'Primary'; - - if (browser.tv || browser.slow) { - - if (browser.chrome) { - // webp support - quality = type === 'Primary' ? 40 : 50; - } else { - quality = type === 'Backdrop' ? 60 : 50; - } - } else { - quality = type === 'Backdrop' ? 70 : 90; - } - - options.quality = quality; + if (setQuality && !options.quality) { + options.quality = 90; } } @@ -272,12 +230,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM if (navigator.connection) { var max = navigator.connection.downlinkMax; if (max && max > 0 && max < Number.POSITIVE_INFINITY) { - max /= 8; max *= 1000000; max *= 0.7; - max = parseInt(max); - return max; + return parseInt(max, 10); } } /* eslint-enable compat/compat */ @@ -290,7 +246,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function onApiClientCreated(e, newApiClient) { - newApiClient.normalizeImageOptions = normalizeImageOptions; if (browser.iOS) { @@ -304,12 +259,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function initApiClient(apiClient) { - onApiClientCreated({}, apiClient); } function initApiClients() { - connectionManager.getApiClients().forEach(initApiClient); events.on(connectionManager, 'apiclientcreated', onApiClientCreated); @@ -325,7 +278,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM var firstConnectionResult; function start(options) { - loading.show(); initApiClients(); @@ -335,56 +287,29 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM connectionManager.connect({ enableAutoLogin: appSettings.enableAutoLogin() - }).then(function (result) { - firstConnectionResult = result; - options = options || {}; - page({ click: options.click !== false, - hashbang: options.hashbang !== false, - enableHistory: enableHistory() + hashbang: options.hashbang !== false }); }).catch().then(function() { loading.hide(); }); } - function enableHistory() { - - //if (browser.edgeUwp) { - // return false; - //} - - // shows status bar on navigation - if (browser.xboxOne) { - return false; - } - - // Does not support history - if (browser.orsay) { - return false; - } - - return true; - } - function enableNativeHistory() { return false; } function authenticate(ctx, route, callback) { - var firstResult = firstConnectionResult; if (firstResult) { - firstConnectionResult = null; if (firstResult.State !== 'SignedIn' && !route.anonymous) { - - handleConnectionResult(firstResult, loading); + handleConnectionResult(firstResult); return; } } @@ -412,19 +337,15 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } if (apiClient && apiClient.isLoggedIn()) { - console.debug('appRouter - user is authenticated'); if (route.isDefaultRoute) { console.debug('appRouter - loading skin home page'); - loadUserSkinWithOptions(ctx); + Emby.Page.goHome(); return; } else if (route.roles) { - validateRoles(apiClient, route.roles).then(function () { - callback(); - }, beginConnectionWizard); return; } @@ -434,15 +355,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM callback(); } - function loadUserSkinWithOptions(ctx) { - require(['queryString'], function (queryString) { - var params = queryString.parse(ctx.querystring); - skinManager.loadUserSkin({ - start: params.start - }); - }); - } - function validateRoles(apiClient, roles) { return Promise.all(roles.split(',').map(function (role) { return validateRole(apiClient, role); @@ -463,12 +375,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM return Promise.resolve(); } - var isHandlingBackToDefault; var isDummyBackToHome; function loadContent(ctx, route, html, request) { - - html = globalize.translateDocument(html, route.dictionary); + html = globalize.translateHtml(html, route.dictionary); request.view = html; viewManager.loadView(request); @@ -527,7 +437,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function getWindowLocationSearch(win) { - var currentPath = currentRouteInfo ? (currentRouteInfo.path || '') : ''; var index = currentPath.indexOf('?'); @@ -571,9 +480,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM if (!document.querySelector('.dialogContainer') && startPages.indexOf(curr.type) !== -1) { return false; } - if (enableHistory()) { - return history.length > 1; - } + return (page.len || 0) > 0; } @@ -589,8 +496,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM path = '/' + path; } - var baseRoute = baseUrl(); - path = path.replace(baseRoute, ''); + path = path.replace(baseUrl(), ''); if (currentRouteInfo && currentRouteInfo.path === path) { // can't use this with home right now due to the back menu @@ -621,10 +527,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function showItem(item, serverId, options) { + // TODO: Refactor this so it only gets items, not strings. if (typeof (item) === 'string') { var apiClient = serverId ? connectionManager.getApiClient(serverId) : connectionManager.currentApiClient(); - apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (item) { - appRouter.showItem(item, options); + apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (itemObject) { + appRouter.showItem(itemObject, options); }); } else { if (arguments.length === 2) { @@ -660,7 +567,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } if (level === 'full' || level === 2) { - backdrop.clear(true); + backdrop.clearBackdrop(true); document.documentElement.classList.add('transparentDocument'); backgroundContainer.classList.add('backgroundContainer-transparent'); backdropContainer.classList.add('hide'); @@ -680,7 +587,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM function pushState(state, title, url) { state.navigate = false; history.pushState(state, title, url); - } function setBaseRoute() { diff --git a/src/components/apphost.js b/src/components/apphost.js index 75e8ba17f1..3ed590b546 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -1,11 +1,13 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'globalize'], function (appSettings, browser, events, htmlMediaHelper, webSettings, globalize) { 'use strict'; + browser = browser.default || browser; + function getBaseProfileOptions(item) { var disableHlsVideoAudioCodecs = []; if (item && htmlMediaHelper.enableHlsJsPlayer(item.RunTimeTicks, item.MediaType)) { - if (browser.edge || browser.msie) { + if (browser.edge) { disableHlsVideoAudioCodecs.push('mp3'); } @@ -47,7 +49,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder); } else { var builderOpts = getBaseProfileOptions(item); - builderOpts.enableSsaRender = (item && !options.isRetry && 'allcomplexformats' !== appSettings.get('subtitleburnin')); + builderOpts.enableSsaRender = (item && !options.isRetry && appSettings.get('subtitleburnin') !== 'allcomplexformats'); profile = profileBuilder(builderOpts); } @@ -93,18 +95,38 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g function getDeviceName() { var deviceName; - deviceName = browser.tizen ? 'Samsung Smart TV' : browser.web0s ? 'LG Smart TV' : browser.operaTv ? 'Opera TV' : browser.xboxOne ? 'Xbox One' : browser.ps4 ? 'Sony PS4' : browser.chrome ? 'Chrome' : browser.edge ? 'Edge' : browser.firefox ? 'Firefox' : browser.msie ? 'Internet Explorer' : browser.opera ? 'Opera' : browser.safari ? 'Safari' : 'Web Browser'; + if (browser.tizen) { + deviceName = 'Samsung Smart TV'; + } else if (browser.web0s) { + deviceName = 'LG Smart TV'; + } else if (browser.operaTv) { + deviceName = 'Opera TV'; + } else if (browser.xboxOne) { + deviceName = 'Xbox One'; + } else if (browser.ps4) { + deviceName = 'Sony PS4'; + } else if (browser.chrome) { + deviceName = 'Chrome'; + } else if (browser.edgeChromium) { + deviceName = 'Edge Chromium'; + } else if (browser.edge) { + deviceName = 'Edge'; + } else if (browser.firefox) { + deviceName = 'Firefox'; + } else if (browser.opera) { + deviceName = 'Opera'; + } else if (browser.safari) { + deviceName = 'Safari'; + } else { + deviceName = 'Web Browser'; + } if (browser.ipad) { deviceName += ' iPad'; - } else { - if (browser.iphone) { - deviceName += ' iPhone'; - } else { - if (browser.android) { - deviceName += ' Android'; - } - } + } else if (browser.iphone) { + deviceName += ' iPhone'; + } else if (browser.android) { + deviceName += ' Android'; } return deviceName; @@ -239,12 +261,6 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g features.push('fullscreenchange'); } - if (browser.chrome || browser.edge && !browser.slow) { - if (!browser.noAnimation && !browser.edgeUwp && !browser.xboxOne) { - features.push('imageanalysis'); - } - } - if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile) { features.push('physicalvolumecontrol'); } @@ -263,11 +279,11 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g features.push('targetblank'); features.push('screensaver'); - webSettings.enableMultiServer().then(enabled => { + webSettings.getMultiServer().then(enabled => { if (enabled) features.push('multiserver'); }); - if (!browser.orsay && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { + if (!browser.orsay && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { features.push('subtitleappearancesettings'); } @@ -279,7 +295,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g features.push('fileinput'); } - if (browser.chrome) { + if (browser.chrome || browser.edgeChromium) { features.push('chromecast'); } @@ -335,7 +351,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g var deviceId; var deviceName; var appName = 'Jellyfin Web'; - var appVersion = '10.6.0'; + var appVersion = '10.7.0'; var appHost = { getWindowState: function () { @@ -356,10 +372,9 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g return window.NativeShell.AppHost.supports(command); } - return -1 !== supportedFeatures.indexOf(command.toLowerCase()); + return supportedFeatures.indexOf(command.toLowerCase()) !== -1; }, preferVisualCards: browser.android || browser.chrome, - moreIcon: browser.android ? 'more_vert' : 'more_horiz', getSyncProfile: getSyncProfile, getDefaultLayout: function () { if (window.NativeShell) { @@ -394,13 +409,6 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g getPushTokenInfo: function () { return {}; }, - setThemeColor: function (color) { - var metaThemeColor = document.querySelector('meta[name=theme-color]'); - - if (metaThemeColor) { - metaThemeColor.setAttribute('content', color); - } - }, setUserScalable: function (scalable) { if (!browser.tv) { var att = scalable ? 'width=device-width, initial-scale=1, minimum-scale=1, user-scalable=yes' : 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no'; diff --git a/src/components/backdrop/backdrop.js b/src/components/backdrop/backdrop.js index c15e35524c..d3c9e58b59 100644 --- a/src/components/backdrop/backdrop.js +++ b/src/components/backdrop/backdrop.js @@ -1,5 +1,11 @@ -define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings', 'css!./backdrop'], function (browser, connectionManager, playbackManager, dom, userSettings) { - 'use strict'; +import browser from 'browser'; +import connectionManager from 'connectionManager'; +import playbackManager from 'playbackManager'; +import dom from 'dom'; +import * as userSettings from 'userSettings'; +import 'css!./backdrop'; + +/* eslint-disable indent */ function enableAnimation(elem) { if (browser.slow) { @@ -22,71 +28,70 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' return true; } - function Backdrop() { - } + class Backdrop { + load(url, parent, existingBackdropImage) { + const img = new Image(); + const self = this; - Backdrop.prototype.load = function (url, parent, existingBackdropImage) { - var img = new Image(); - var self = this; - - img.onload = function () { - if (self.isDestroyed) { - return; - } - - var backdropImage = document.createElement('div'); - backdropImage.classList.add('backdropImage'); - backdropImage.classList.add('displayingBackdropImage'); - backdropImage.style.backgroundImage = "url('" + url + "')"; - backdropImage.setAttribute('data-url', url); - - backdropImage.classList.add('backdropImageFadeIn'); - parent.appendChild(backdropImage); - - if (!enableAnimation(backdropImage)) { - if (existingBackdropImage && existingBackdropImage.parentNode) { - existingBackdropImage.parentNode.removeChild(existingBackdropImage); + img.onload = () => { + if (self.isDestroyed) { + return; } - internalBackdrop(true); - return; - } - var onAnimationComplete = function () { - dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, { + const backdropImage = document.createElement('div'); + backdropImage.classList.add('backdropImage'); + backdropImage.classList.add('displayingBackdropImage'); + backdropImage.style.backgroundImage = `url('${url}')`; + backdropImage.setAttribute('data-url', url); + + backdropImage.classList.add('backdropImageFadeIn'); + parent.appendChild(backdropImage); + + if (!enableAnimation(backdropImage)) { + if (existingBackdropImage && existingBackdropImage.parentNode) { + existingBackdropImage.parentNode.removeChild(existingBackdropImage); + } + internalBackdrop(true); + return; + } + + const onAnimationComplete = () => { + dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, { + once: true + }); + if (backdropImage === self.currentAnimatingElement) { + self.currentAnimatingElement = null; + } + if (existingBackdropImage && existingBackdropImage.parentNode) { + existingBackdropImage.parentNode.removeChild(existingBackdropImage); + } + }; + + dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, { once: true }); - if (backdropImage === self.currentAnimatingElement) { - self.currentAnimatingElement = null; - } - if (existingBackdropImage && existingBackdropImage.parentNode) { - existingBackdropImage.parentNode.removeChild(existingBackdropImage); - } + + internalBackdrop(true); }; - dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, { - once: true - }); - - internalBackdrop(true); - }; - - img.src = url; - }; - - Backdrop.prototype.cancelAnimation = function () { - var elem = this.currentAnimatingElement; - if (elem) { - elem.classList.remove('backdropImageFadeIn'); - this.currentAnimatingElement = null; + img.src = url; } - }; - Backdrop.prototype.destroy = function () { - this.isDestroyed = true; - this.cancelAnimation(); - }; + cancelAnimation() { + const elem = this.currentAnimatingElement; + if (elem) { + elem.classList.remove('backdropImageFadeIn'); + this.currentAnimatingElement = null; + } + } - var backdropContainer; + destroy() { + this.isDestroyed = true; + this.cancelAnimation(); + } + } + + let backdropContainer; function getBackdropContainer() { if (!backdropContainer) { backdropContainer = document.querySelector('.backdropContainer'); @@ -101,7 +106,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' return backdropContainer; } - function clearBackdrop(clearAll) { + export function clearBackdrop(clearAll) { clearRotation(); if (currentLoadingBackdrop) { @@ -109,7 +114,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' currentLoadingBackdrop = null; } - var elem = getBackdropContainer(); + const elem = getBackdropContainer(); elem.innerHTML = ''; if (clearAll) { @@ -119,7 +124,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' internalBackdrop(false); } - var backgroundContainer; + let backgroundContainer; function getBackgroundContainer() { if (!backgroundContainer) { backgroundContainer = document.querySelector('.backgroundContainer'); @@ -135,31 +140,27 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' } } - var hasInternalBackdrop; + let hasInternalBackdrop; function internalBackdrop(enabled) { hasInternalBackdrop = enabled; setBackgroundContainerBackgroundEnabled(); } - var hasExternalBackdrop; - function externalBackdrop(enabled) { + let hasExternalBackdrop; + export function externalBackdrop(enabled) { hasExternalBackdrop = enabled; setBackgroundContainerBackgroundEnabled(); } - function getRandom(min, max) { - return Math.floor(Math.random() * (max - min) + min); - } - - var currentLoadingBackdrop; + let currentLoadingBackdrop; function setBackdropImage(url) { if (currentLoadingBackdrop) { currentLoadingBackdrop.destroy(); currentLoadingBackdrop = null; } - var elem = getBackdropContainer(); - var existingBackdropImage = elem.querySelector('.displayingBackdropImage'); + const elem = getBackdropContainer(); + const existingBackdropImage = elem.querySelector('.displayingBackdropImage'); if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) { if (existingBackdropImage.getAttribute('data-url') === url) { @@ -168,7 +169,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' existingBackdropImage.classList.remove('displayingBackdropImage'); } - var instance = new Backdrop(); + const instance = new Backdrop(); instance.load(url, elem, existingBackdropImage); currentLoadingBackdrop = instance; } @@ -176,9 +177,9 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' function getItemImageUrls(item, imageOptions) { imageOptions = imageOptions || {}; - var apiClient = connectionManager.getApiClient(item.ServerId); + const apiClient = connectionManager.getApiClient(item.ServerId); if (item.BackdropImageTags && item.BackdropImageTags.length > 0) { - return item.BackdropImageTags.map(function (imgTag, index) { + return item.BackdropImageTags.map((imgTag, index) => { return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, { type: 'Backdrop', tag: imgTag, @@ -189,7 +190,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' } if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { - return item.ParentBackdropImageTags.map(function (imgTag, index) { + return item.ParentBackdropImageTags.map((imgTag, index) => { return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, { type: 'Backdrop', tag: imgTag, @@ -203,13 +204,13 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' } function getImageUrls(items, imageOptions) { - var list = []; - var onImg = function (img) { + const list = []; + const onImg = img => { list.push(img); }; - for (var i = 0, length = items.length; i < length; i++) { - var itemImages = getItemImageUrls(items[i], imageOptions); + for (let i = 0, length = items.length; i < length; i++) { + const itemImages = getItemImageUrls(items[i], imageOptions); itemImages.forEach(onImg); } @@ -229,7 +230,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' // If you don't care about the order of the elements inside // the array, you should sort both arrays here. - for (var i = 0; i < a.length; ++i) { + for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } @@ -242,12 +243,12 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' return userSettings.enableBackdrops(); } - var rotationInterval; - var currentRotatingImages = []; - var currentRotationIndex = -1; - function setBackdrops(items, imageOptions, enableImageRotation) { + let rotationInterval; + let currentRotatingImages = []; + let currentRotationIndex = -1; + export function setBackdrops(items, imageOptions, enableImageRotation) { if (enabled()) { - var images = getImageUrls(items, imageOptions); + const images = getImageUrls(items, imageOptions); if (images.length) { startRotation(images, enableImageRotation); @@ -279,7 +280,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' return; } - var newIndex = currentRotationIndex + 1; + let newIndex = currentRotationIndex + 1; if (newIndex >= currentRotatingImages.length) { newIndex = 0; } @@ -289,7 +290,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' } function clearRotation() { - var interval = rotationInterval; + const interval = rotationInterval; if (interval) { clearInterval(interval); } @@ -299,7 +300,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' currentRotationIndex = -1; } - function setBackdrop(url, imageOptions) { + export function setBackdrop(url, imageOptions) { if (url && typeof url !== 'string') { url = getImageUrls([url], imageOptions)[0]; } @@ -312,10 +313,11 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', 'userSettings' } } - return { - setBackdrops: setBackdrops, - setBackdrop: setBackdrop, - clear: clearBackdrop, - externalBackdrop: externalBackdrop - }; -}); +/* eslint-enable indent */ + +export default { + setBackdrops: setBackdrops, + setBackdrop: setBackdrop, + clearBackdrop: clearBackdrop, + externalBackdrop: externalBackdrop +}; diff --git a/src/components/backdropscreensaver/plugin.js b/src/components/backdropscreensaver/plugin.js deleted file mode 100644 index dc0a906ddb..0000000000 --- a/src/components/backdropscreensaver/plugin.js +++ /dev/null @@ -1,56 +0,0 @@ -define(['connectionManager'], function (connectionManager) { - - return function () { - - var self = this; - - self.name = 'Backdrop ScreenSaver'; - self.type = 'screensaver'; - self.id = 'backdropscreensaver'; - self.supportsAnonymous = false; - - var currentSlideshow; - - self.show = function () { - - var query = { - ImageTypes: 'Backdrop', - EnableImageTypes: 'Backdrop', - IncludeItemTypes: 'Movie,Series,MusicArtist', - SortBy: 'Random', - Recursive: true, - Fields: 'Taglines', - ImageTypeLimit: 1, - StartIndex: 0, - Limit: 200 - }; - - var apiClient = connectionManager.currentApiClient(); - apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) { - - if (result.Items.length) { - - require(['slideshow'], function (slideshow) { - - var newSlideShow = new slideshow({ - showTitle: true, - cover: true, - items: result.Items - }); - - newSlideShow.show(); - currentSlideshow = newSlideShow; - }); - } - }); - }; - - self.hide = function () { - - if (currentSlideshow) { - currentSlideshow.hide(); - currentSlideshow = null; - } - }; - }; -}); diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index 3cd038cd09..ef5ea6604c 100644 --- a/src/components/cardbuilder/card.css +++ b/src/components/cardbuilder/card.css @@ -167,8 +167,9 @@ button::-moz-focus-inner { position: relative; background-clip: content-box !important; color: inherit; +} - /* This is only needed for scalable cards */ +.cardScalable .cardImageContainer { height: 100%; contain: strict; } @@ -192,9 +193,14 @@ button::-moz-focus-inner { /* Needed in case this is a button */ display: block; - - /* Needed in case this is a button */ margin: 0 !important; + border: 0 !important; + padding: 0 !important; + cursor: pointer; + color: inherit; + width: 100%; + font-family: inherit; + font-size: inherit; /* Needed in safari */ height: 100%; @@ -203,19 +209,12 @@ button::-moz-focus-inner { contain: strict; } -.cardContent-button { - border: 0 !important; - padding: 0 !important; - cursor: pointer; - color: inherit; - width: 100%; - vertical-align: middle; - font-family: inherit; - font-size: inherit; +.cardContent:not(.defaultCardBackground) { + background-color: transparent; } -.cardContent-button:not(.defaultCardBackground) { - background-color: transparent; +.cardBox:not(.visualCardBox) .cardPadder { + background-color: #242424; } .visualCardBox .cardContent { @@ -223,7 +222,8 @@ button::-moz-focus-inner { border-bottom-right-radius: 0; } -.cardContent-shadow { +.cardContent-shadow, +.cardBox:not(.visualCardBox) .cardPadder { box-shadow: 0 0.0725em 0.29em 0 rgba(0, 0, 0, 0.37); } @@ -306,6 +306,10 @@ button::-moz-focus-inner { text-align: left; } +.dialog .cardText { + text-overflow: initial; +} + .cardText-secondary { font-size: 86%; } diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 43ca28f01d..4a37331ef4 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -277,7 +277,7 @@ import 'programStyles'; */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); - return Math.round(screenWidth / imagesPerRow) * 2; + return Math.round(screenWidth / imagesPerRow); } /** @@ -291,12 +291,10 @@ import 'programStyles'; const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); if (['auto', 'autohome', 'autooverflow', 'autoVertical'].includes(options.shape)) { - const requestedShape = options.shape; options.shape = null; if (primaryImageAspectRatio) { - if (primaryImageAspectRatio >= 3) { options.shape = 'banner'; options.coverImage = true; @@ -368,9 +366,7 @@ import 'programStyles'; let apiClient; let lastServerId; - for (let i = 0; i < items.length; i++) { - - let item = items[i]; + for (const [i, item] of items.entries()) { let serverId = item.ServerId || options.serverId; if (serverId !== lastServerId) { @@ -396,7 +392,6 @@ import 'programStyles'; } if (newIndexValue !== currentIndexValue) { - if (hasOpenRow) { html += '

'; hasOpenRow = false; @@ -404,7 +399,6 @@ import 'programStyles'; } if (hasOpenSection) { - html += ''; if (isVertical) { @@ -428,7 +422,6 @@ import 'programStyles'; } if (options.rows && itemsInRow === 0) { - if (hasOpenRow) { html += ''; hasOpenRow = false; @@ -503,94 +496,49 @@ import 'programStyles'; const primaryImageAspectRatio = item.PrimaryImageAspectRatio; let forceName = false; let imgUrl = null; + let imgTag = null; let coverImage = false; let uiAspect = null; + let imgType = null; + let itemId = null; if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Thumb', - maxWidth: width, - tag: item.ImageTags.Thumb - }); - + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; } else if ((options.preferBanner || shape === 'banner') && item.ImageTags && item.ImageTags.Banner) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Banner', - maxWidth: width, - tag: item.ImageTags.Banner - }); - + imgType = 'Banner'; + imgTag = item.ImageTags.Banner; } else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Disc', - maxWidth: width, - tag: item.ImageTags.Disc - }); - + imgType = 'Disc'; + imgTag = item.ImageTags.Disc; } else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Logo', - maxWidth: width, - tag: item.ImageTags.Logo - }); - + imgType = 'Logo'; + imgTag = item.ImageTags.Logo; } else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) { - - imgUrl = apiClient.getScaledImageUrl(item.ParentLogoItemId, { - type: 'Logo', - maxWidth: width, - tag: item.ParentLogoImageTag - }); - + imgType = 'Logo'; + imgTag = item.ParentLogoImageTag; + itemId = item.ParentLogoItemId; } else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) { - - imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { - type: 'Thumb', - maxWidth: width, - tag: item.SeriesThumbImageTag - }); - + imgType = 'Thumb'; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; } else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') { - - imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { - type: 'Thumb', - maxWidth: width, - tag: item.ParentThumbImageTag - }); - + imgType = 'Thumb'; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; } else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Backdrop', - maxWidth: width, - tag: item.BackdropImageTags[0] - }); - + imgType = 'Backdrop'; + imgTag = item.BackdropImageTags[0]; forceName = true; - } else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') { - - imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { - type: 'Backdrop', - maxWidth: width, - tag: item.ParentBackdropImageTags[0] - }); - - } else if (item.ImageTags && item.ImageTags.Primary) { - + imgType = 'Backdrop'; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; + } else if (item.ImageTags && item.ImageTags.Primary && (item.Type !== 'Episode' || item.ChildCount !== 0)) { + imgType = 'Primary'; + imgTag = item.ImageTags.Primary; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Primary', - maxHeight: height, - maxWidth: width, - tag: item.ImageTags.Primary - }); - if (options.preferThumb && options.showTitle !== false) { forceName = true; } @@ -601,18 +549,16 @@ import 'programStyles'; coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2; } } - + } else if (item.SeriesPrimaryImageTag) { + imgType = 'Primary'; + imgTag = item.SeriesPrimaryImageTag; + itemId = item.SeriesId; } else if (item.PrimaryImageTag) { - + imgType = 'Primary'; + imgTag = item.PrimaryImageTag; + itemId = item.PrimaryImageItemId; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; - imgUrl = apiClient.getScaledImageUrl(item.PrimaryImageItemId || item.Id || item.ItemId, { - type: 'Primary', - maxHeight: height, - maxWidth: width, - tag: item.PrimaryImageTag - }); - if (options.preferThumb && options.showTitle !== false) { forceName = true; } @@ -624,30 +570,15 @@ import 'programStyles'; } } } else if (item.ParentPrimaryImageTag) { - - imgUrl = apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, { - type: 'Primary', - maxWidth: width, - tag: item.ParentPrimaryImageTag - }); - } else if (item.SeriesPrimaryImageTag) { - - imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { - type: 'Primary', - maxWidth: width, - tag: item.SeriesPrimaryImageTag - }); + imgType = 'Primary'; + imgTag = item.ParentPrimaryImageTag; + itemId = item.ParentPrimaryImageItemId; } else if (item.AlbumId && item.AlbumPrimaryImageTag) { - + imgType = 'Primary'; + imgTag = item.AlbumPrimaryImageTag; + itemId = item.AlbumId; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; - imgUrl = apiClient.getScaledImageUrl(item.AlbumId, { - type: 'Primary', - maxHeight: height, - maxWidth: width, - tag: item.AlbumPrimaryImageTag - }); - if (primaryImageAspectRatio) { uiAspect = getDesiredAspect(shape); if (uiAspect) { @@ -655,57 +586,46 @@ import 'programStyles'; } } } else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Thumb', - maxWidth: width, - tag: item.ImageTags.Thumb - }); - + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; } else if (item.BackdropImageTags && item.BackdropImageTags.length) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Backdrop', - maxWidth: width, - tag: item.BackdropImageTags[0] - }); - + imgType = 'Backdrop'; + imgTag = item.BackdropImageTags[0]; } else if (item.ImageTags && item.ImageTags.Thumb) { - - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Thumb', - maxWidth: width, - tag: item.ImageTags.Thumb - }); - + imgType = 'Thumb'; + imgTag = item.ImageTags.Thumb; } else if (item.SeriesThumbImageTag && options.inheritThumb !== false) { - - imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { - type: 'Thumb', - maxWidth: width, - tag: item.SeriesThumbImageTag - }); - + imgType = 'Thumb'; + imgTag = item.SeriesThumbImageTag; + itemId = item.SeriesId; } else if (item.ParentThumbItemId && options.inheritThumb !== false) { - - imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { - type: 'Thumb', - maxWidth: width, - tag: item.ParentThumbImageTag - }); - + imgType = 'Thumb'; + imgTag = item.ParentThumbImageTag; + itemId = item.ParentThumbItemId; } else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) { - - imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { - type: 'Backdrop', - maxWidth: width, - tag: item.ParentBackdropImageTags[0] - }); - + imgType = 'Backdrop'; + imgTag = item.ParentBackdropImageTags[0]; + itemId = item.ParentBackdropItemId; } + if (!itemId) { + itemId = item.Id; + } + + if (imgTag && imgType) { + imgUrl = apiClient.getScaledImageUrl(itemId, { + type: imgType, + maxHeight: height, + maxWidth: width, + tag: imgTag + }); + } + + let blurHashes = options.imageBlurhashes || item.ImageBlurHashes || {}; + return { imgUrl: imgUrl, + blurhash: (blurHashes[imgType] || {})[imgTag], forceName: forceName, coverImage: coverImage }; @@ -761,7 +681,6 @@ import 'programStyles'; let valid = 0; for (let i = 0; i < lines.length; i++) { - let currentCssClass = cssClass; let text = lines[i]; @@ -788,7 +707,6 @@ import 'programStyles'; } if (forceLines) { - let linesLength = maxLines || Math.min(lines.length, maxLines || lines.length); while (valid < linesLength) { @@ -820,7 +738,6 @@ import 'programStyles'; let airTimeText = ''; if (item.StartDate) { - try { let date = datetime.parseISO8601Date(item.StartDate); @@ -867,9 +784,8 @@ import 'programStyles'; const showOtherText = isOuterFooter ? !overlayText : overlayText; if (isOuterFooter && options.cardLayout && layoutManager.mobile) { - if (options.cardFooterAside !== 'none') { - html += ''; + html += ''; } } @@ -882,9 +798,7 @@ import 'programStyles'; if (showOtherText) { if ((options.showParentTitle || options.showParentTitleOrTitle) && !parentTitleUnderneath) { - if (isOuterFooter && item.Type === 'Episode' && item.SeriesName) { - if (item.SeriesId) { lines.push(getTextActionButton({ Id: item.SeriesId, @@ -897,15 +811,12 @@ import 'programStyles'; lines.push(item.SeriesName); } } else { - if (isUsingLiveTvNaming(item)) { - lines.push(item.Name); if (!item.EpisodeTitle) { titleAdded = true; } - } else { const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''; @@ -923,7 +834,6 @@ import 'programStyles'; } if (showMediaTitle) { - const name = options.showTitle === 'auto' && !item.IsFolder && item.MediaType === 'Photo' ? '' : itemHelper.getDisplayName(item, { includeParentInfo: options.includeParentInfoInTitle }); @@ -940,7 +850,6 @@ import 'programStyles'; if (showOtherText) { if (options.showParentTitle && parentTitleUnderneath) { - if (isOuterFooter && item.AlbumArtists && item.AlbumArtists.length) { item.AlbumArtists[0].Type = 'MusicArtist'; item.AlbumArtists[0].IsFolder = true; @@ -974,7 +883,6 @@ import 'programStyles'; } if (options.showPremiereDate) { - if (item.PremiereDate) { try { lines.push(datetime.toLocaleDateString( @@ -983,7 +891,6 @@ import 'programStyles'; )); } catch (err) { lines.push(''); - } } else { lines.push(''); @@ -991,14 +898,10 @@ import 'programStyles'; } if (options.showYear || options.showSeriesYear) { - if (item.Type === 'Series') { if (item.Status === 'Continuing') { - lines.push(globalize.translate('SeriesYearToPresent', item.ProductionYear || '')); - } else { - if (item.EndDate && item.ProductionYear) { const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); lines.push(item.ProductionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); @@ -1012,9 +915,7 @@ import 'programStyles'; } if (options.showRuntime) { - if (item.RunTimeTicks) { - lines.push(datetime.getDisplayRunningTime(item.RunTimeTicks)); } else { lines.push(''); @@ -1022,14 +923,11 @@ import 'programStyles'; } if (options.showAirTime) { - lines.push(getAirTimeText(item, options.showAirDateTime, options.showAirEndTime) || ''); } if (options.showChannelName) { - if (item.ChannelId) { - lines.push(getTextActionButton({ Id: item.ChannelId, @@ -1046,7 +944,6 @@ import 'programStyles'; } if (options.showCurrentProgram && item.Type === 'TvChannel') { - if (item.CurrentProgram) { lines.push(item.CurrentProgram.Name); } else { @@ -1055,7 +952,6 @@ import 'programStyles'; } if (options.showCurrentProgramTime && item.Type === 'TvChannel') { - if (item.CurrentProgram) { lines.push(getAirTimeText(item.CurrentProgram, false, true) || ''); } else { @@ -1065,7 +961,6 @@ import 'programStyles'; if (options.showSeriesTimerTime) { if (item.RecordAnyTime) { - lines.push(globalize.translate('Anytime')); } else { lines.push(datetime.getDisplayTime(item.StartDate)); @@ -1100,7 +995,6 @@ import 'programStyles'; } if (html) { - if (!isOuterFooter || logoUrl || options.cardLayout) { html = '
' + html; @@ -1146,27 +1040,21 @@ import 'programStyles'; let childText; if (item.Type === 'Playlist') { - childText = ''; if (item.RunTimeTicks) { - let minutes = item.RunTimeTicks / 600000000; minutes = minutes || 1; childText += globalize.translate('ValueMinutes', Math.round(minutes)); - } else { childText += globalize.translate('ValueMinutes', 0); } counts.push(childText); - } else if (item.Type === 'Genre' || item.Type === 'Studio') { - if (item.MovieCount) { - childText = item.MovieCount === 1 ? globalize.translate('ValueOneMovie') : globalize.translate('ValueMovieCount', item.MovieCount); @@ -1175,7 +1063,6 @@ import 'programStyles'; } if (item.SeriesCount) { - childText = item.SeriesCount === 1 ? globalize.translate('ValueOneSeries') : globalize.translate('ValueSeriesCount', item.SeriesCount); @@ -1183,18 +1070,14 @@ import 'programStyles'; counts.push(childText); } if (item.EpisodeCount) { - childText = item.EpisodeCount === 1 ? globalize.translate('ValueOneEpisode') : globalize.translate('ValueEpisodeCount', item.EpisodeCount); counts.push(childText); } - } else if (item.Type === 'MusicGenre' || options.context === 'MusicArtist') { - if (item.AlbumCount) { - childText = item.AlbumCount === 1 ? globalize.translate('ValueOneAlbum') : globalize.translate('ValueAlbumCount', item.AlbumCount); @@ -1202,7 +1085,6 @@ import 'programStyles'; counts.push(childText); } if (item.SongCount) { - childText = item.SongCount === 1 ? globalize.translate('ValueOneSong') : globalize.translate('ValueSongCount', item.SongCount); @@ -1210,16 +1092,13 @@ import 'programStyles'; counts.push(childText); } if (item.MusicVideoCount) { - childText = item.MusicVideoCount === 1 ? globalize.translate('ValueOneMusicVideo') : globalize.translate('ValueMusicVideoCount', item.MusicVideoCount); counts.push(childText); } - } else if (item.Type === 'Series') { - childText = item.RecursiveItemCount === 1 ? globalize.translate('ValueOneEpisode') : globalize.translate('ValueEpisodeCount', item.RecursiveItemCount); @@ -1235,10 +1114,11 @@ import 'programStyles'; /** * Imports the refresh indicator element. */ - function requireRefreshIndicator() { + function importRefreshIndicator() { if (!refreshIndicatorLoaded) { refreshIndicatorLoaded = true; - require(['emby-itemrefreshindicator']); + /* eslint-disable-next-line no-unused-expressions */ + import('emby-itemrefreshindicator'); } } @@ -1272,13 +1152,11 @@ import 'programStyles'; let shape = options.shape; if (shape === 'mixed') { - shape = null; const primaryImageAspectRatio = item.PrimaryImageAspectRatio; if (primaryImageAspectRatio) { - if (primaryImageAspectRatio >= 1.33) { shape = 'mixedBackdrop'; } else if (primaryImageAspectRatio > 0.71) { @@ -1321,6 +1199,7 @@ import 'programStyles'; const imgInfo = getCardImageUrl(item, apiClient, options, shape); const imgUrl = imgInfo.imgUrl; + const blurhash = imgInfo.blurhash; const forceName = imgInfo.forceName; @@ -1369,7 +1248,6 @@ import 'programStyles'; } if (overlayText) { - logoUrl = null; footerCssClass = progressHtml ? 'innerCardFooter fullInnerCardFooter' : 'innerCardFooter'; @@ -1384,7 +1262,7 @@ import 'programStyles'; } const mediaSourceCount = item.MediaSourceCount || 1; - if (mediaSourceCount > 1) { + if (mediaSourceCount > 1 && options.disableIndicators !== true) { innerCardFooter += '
' + mediaSourceCount + '
'; } @@ -1426,7 +1304,7 @@ import 'programStyles'; } if (options.overlayMoreButton) { - overlayButtons += ''; + overlayButtons += ''; } } @@ -1441,57 +1319,59 @@ import 'programStyles'; let cardScalableClose = ''; let cardContentClass = 'cardContent'; - if (!options.cardLayout) { - cardContentClass += ' cardContent-shadow'; + + let blurhashAttrib = ''; + if (blurhash && blurhash.length > 0) { + blurhashAttrib = 'data-blurhash="' + blurhash + '"'; } if (layoutManager.tv) { - // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? ('
') : ('
'); + cardImageContainerOpen = imgUrl ? ('
') : ('
'); cardImageContainerClose = '
'; } else { // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? (''; } let cardScalableClass = 'cardScalable'; - cardImageContainerOpen = '
' + cardImageContainerOpen; + cardImageContainerOpen = '
' + cardImageContainerOpen; cardBoxClose = '
'; cardScalableClose = '
'; - let indicatorsHtml = ''; + if (options.disableIndicators !== true) { + let indicatorsHtml = ''; - if (options.missingIndicator !== false) { - indicatorsHtml += indicators.getMissingIndicator(item); - } + if (options.missingIndicator !== false) { + indicatorsHtml += indicators.getMissingIndicator(item); + } - indicatorsHtml += indicators.getSyncIndicator(item); - indicatorsHtml += indicators.getTimerIndicator(item); + indicatorsHtml += indicators.getSyncIndicator(item); + indicatorsHtml += indicators.getTimerIndicator(item); - indicatorsHtml += indicators.getTypeIndicator(item); + indicatorsHtml += indicators.getTypeIndicator(item); - if (options.showGroupCount) { + if (options.showGroupCount) { + indicatorsHtml += indicators.getChildCountIndicatorHtml(item, { + minCount: 1 + }); + } else { + indicatorsHtml += indicators.getPlayedIndicatorHtml(item); + } - indicatorsHtml += indicators.getChildCountIndicatorHtml(item, { - minCount: 1 - }); - } else { - indicatorsHtml += indicators.getPlayedIndicatorHtml(item); - } + if (item.Type === 'CollectionFolder' || item.CollectionType) { + const refreshClass = item.RefreshProgress ? '' : ' class="hide"'; + indicatorsHtml += '
'; + importRefreshIndicator(); + } - if (item.Type === 'CollectionFolder' || item.CollectionType) { - const refreshClass = item.RefreshProgress ? '' : ' class="hide"'; - indicatorsHtml += '
'; - requireRefreshIndicator(); - } - - if (indicatorsHtml) { - cardImageContainerOpen += '
' + indicatorsHtml + '
'; + if (indicatorsHtml) { + cardImageContainerOpen += '
' + indicatorsHtml + '
'; + } } if (!imgUrl) { @@ -1539,8 +1419,8 @@ import 'programStyles'; let additionalCardContent = ''; - if (layoutManager.desktop) { - additionalCardContent += getHoverMenuHtml(item, action); + if (layoutManager.desktop && !options.disableHoverMenu) { + additionalCardContent += getHoverMenuHtml(item, action, options); } return '<' + tagName + ' data-index="' + index + '"' + timerAttributes + actionAttribute + ' data-isfolder="' + (item.IsFolder || false) + '" data-serverid="' + (item.ServerId || options.serverId) + '" data-id="' + (item.Id || item.ItemId) + '" data-type="' + item.Type + '"' + mediaTypeData + collectionTypeData + channelIdData + positionTicksData + collectionIdData + playlistIdData + contextData + parentIdData + ' data-prefix="' + prefix + '" class="' + className + '">' + cardImageContainerOpen + innerCardFooter + cardImageContainerClose + overlayButtons + additionalCardContent + cardScalableClose + outerCardFooter + cardBoxClose + ''; @@ -1550,9 +1430,10 @@ import 'programStyles'; * Generates HTML markup for the card overlay. * @param {object} item - Item used to generate the card overlay. * @param {string} action - Action assigned to the overlay. + * @param {Array} options - Card builder options. * @returns {string} HTML markup of the card overlay. */ - function getHoverMenuHtml(item, action) { + function getHoverMenuHtml(item, action, options) { let html = ''; html += '
'; @@ -1568,20 +1449,20 @@ import 'programStyles'; const userData = item.UserData || {}; if (itemHelper.canMarkPlayed(item)) { - require(['emby-playstatebutton']); + /* eslint-disable-next-line no-unused-expressions */ + import('emby-playstatebutton'); html += ''; } if (itemHelper.canRate(item)) { - const likes = userData.Likes == null ? '' : userData.Likes; - require(['emby-ratingbutton']); + /* eslint-disable-next-line no-unused-expressions */ + import('emby-ratingbutton'); html += ''; } - html += ''; - + html += ''; html += '
'; html += '
'; @@ -1605,6 +1486,8 @@ import 'programStyles'; case 'MusicArtist': case 'Person': return ''; + case 'Audio': + return ''; case 'Movie': return ''; case 'Series': @@ -1613,6 +1496,12 @@ import 'programStyles'; return ''; case 'Folder': return ''; + case 'BoxSet': + return ''; + case 'Playlist': + return ''; + case 'PhotoAlbum': + return ''; } if (options && options.defaultCardImageIcon) { @@ -1646,7 +1535,6 @@ import 'programStyles'; const html = buildCardsHtmlInternal(items, options); if (html) { - if (options.itemsContainer.cardBuilderHtml !== html) { options.itemsContainer.innerHTML = html; @@ -1659,7 +1547,6 @@ import 'programStyles'; imageLoader.lazyChildren(options.itemsContainer); } else { - options.itemsContainer.innerHTML = html; options.itemsContainer.cardBuilderHtml = null; } @@ -1683,7 +1570,6 @@ import 'programStyles'; indicatorsElem = card.querySelector('.cardIndicators'); if (!indicatorsElem) { - const cardImageContainer = card.querySelector('.cardImageContainer'); indicatorsElem = document.createElement('div'); indicatorsElem.classList.add('cardIndicators'); @@ -1707,11 +1593,9 @@ import 'programStyles'; let itemProgressBar = null; if (userData.Played) { - playedIndicator = card.querySelector('.playedIndicator'); if (!playedIndicator) { - playedIndicator = document.createElement('div'); playedIndicator.classList.add('playedIndicator'); playedIndicator.classList.add('indicator'); @@ -1720,10 +1604,8 @@ import 'programStyles'; } playedIndicator.innerHTML = ''; } else { - playedIndicator = card.querySelector('.playedIndicator'); if (playedIndicator) { - playedIndicator.parentNode.removeChild(playedIndicator); } } @@ -1731,7 +1613,6 @@ import 'programStyles'; countIndicator = card.querySelector('.countIndicator'); if (!countIndicator) { - countIndicator = document.createElement('div'); countIndicator.classList.add('countIndicator'); indicatorsElem = ensureIndicators(card, indicatorsElem); @@ -1739,10 +1620,8 @@ import 'programStyles'; } countIndicator.innerHTML = userData.UnplayedItemCount; } else if (enableCountIndicator) { - countIndicator = card.querySelector('.countIndicator'); if (countIndicator) { - countIndicator.parentNode.removeChild(countIndicator); } } @@ -1754,7 +1633,6 @@ import 'programStyles'; }); if (progressHtml) { - itemProgressBar = card.querySelector('.itemProgressBar'); if (!itemProgressBar) { @@ -1773,7 +1651,6 @@ import 'programStyles'; itemProgressBar.innerHTML = progressHtml; } else { - itemProgressBar = card.querySelector('.itemProgressBar'); if (itemProgressBar) { itemProgressBar.parentNode.removeChild(itemProgressBar); diff --git a/src/components/cardbuilder/chaptercardbuilder.js b/src/components/cardbuilder/chaptercardbuilder.js index eae60574b3..642a87db2a 100644 --- a/src/components/cardbuilder/chaptercardbuilder.js +++ b/src/components/cardbuilder/chaptercardbuilder.js @@ -1,13 +1,22 @@ -define(['datetime', 'imageLoader', 'connectionManager', 'layoutManager', 'browser'], function (datetime, imageLoader, connectionManager, layoutManager, browser) { - 'use strict'; +/* eslint-disable indent */ - var enableFocusTransform = !browser.slow && !browser.edge; +/** + * Module for building cards from item data. + * @module components/cardBuilder/chaptercardbuilder + */ - function buildChapterCardsHtml(item, chapters, options) { +import datetime from 'datetime'; +import imageLoader from 'imageLoader'; +import connectionManager from 'connectionManager'; +import layoutManager from 'layoutManager'; +import browser from 'browser'; + const enableFocusTransform = !browser.slow && !browser.edge; + + function buildChapterCardsHtml(item, chapters, options) { // TODO move card creation code to Card component - var className = 'card itemAction chapterCard'; + let className = 'card itemAction chapterCard'; if (layoutManager.tv) { className += ' show-focus'; @@ -17,38 +26,36 @@ define(['datetime', 'imageLoader', 'connectionManager', 'layoutManager', 'browse } } - var mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || []; - var videoStream = mediaStreams.filter(function (i) { - return i.Type === 'Video'; + const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || []; + const videoStream = mediaStreams.filter(({Type}) => { + return Type === 'Video'; })[0] || {}; - var shape = (options.backdropShape || 'backdrop'); + let shape = (options.backdropShape || 'backdrop'); if (videoStream.Width && videoStream.Height) { - if ((videoStream.Width / videoStream.Height) <= 1.2) { shape = (options.squareShape || 'square'); } } - className += ' ' + shape + 'Card'; + className += ` ${shape}Card`; if (options.block || options.rows) { className += ' block'; } - var html = ''; - var itemsInRow = 0; + let html = ''; + let itemsInRow = 0; - var apiClient = connectionManager.getApiClient(item.ServerId); - - for (var i = 0, length = chapters.length; i < length; i++) { + const apiClient = connectionManager.getApiClient(item.ServerId); + for (let i = 0, length = chapters.length; i < length; i++) { if (options.rows && itemsInRow === 0) { html += '
'; } - var chapter = chapters[i]; + const chapter = chapters[i]; html += buildChapterCard(item, apiClient, chapter, i, options, className, shape); itemsInRow++; @@ -62,51 +69,45 @@ define(['datetime', 'imageLoader', 'connectionManager', 'layoutManager', 'browse return html; } - function getImgUrl(item, chapter, index, maxWidth, apiClient) { + function getImgUrl({Id}, {ImageTag}, index, maxWidth, apiClient) { + if (ImageTag) { + return apiClient.getScaledImageUrl(Id, { - if (chapter.ImageTag) { - - return apiClient.getScaledImageUrl(item.Id, { - - maxWidth: maxWidth * 2, - tag: chapter.ImageTag, + maxWidth: maxWidth, + tag: ImageTag, type: 'Chapter', - index: index + index }); } return null; } - function buildChapterCard(item, apiClient, chapter, index, options, className, shape) { + function buildChapterCard(item, apiClient, chapter, index, {width, coverImage}, className, shape) { + const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient); - var imgUrl = getImgUrl(item, chapter, index, options.width || 400, apiClient); - - var cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer'; - if (options.coverImage) { + let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer'; + if (coverImage) { cardImageContainerClass += ' coveredImage'; } - var dataAttributes = ' data-action="play" data-isfolder="' + item.IsFolder + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-type="' + item.Type + '" data-mediatype="' + item.MediaType + '" data-positionticks="' + chapter.StartPositionTicks + '"'; - var cardImageContainer = imgUrl ? ('
') : ('
'); + const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`; + let cardImageContainer = imgUrl ? (`
`) : (`
`); if (!imgUrl) { cardImageContainer += ''; } - var nameHtml = ''; - nameHtml += '
' + chapter.Name + '
'; - nameHtml += '
' + datetime.getDisplayRunningTime(chapter.StartPositionTicks) + '
'; + let nameHtml = ''; + nameHtml += `
${chapter.Name}
`; + nameHtml += `
${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}
`; - var cardBoxCssClass = 'cardBox'; - var cardScalableClass = 'cardScalable'; + const cardBoxCssClass = 'cardBox'; + const cardScalableClass = 'cardScalable'; - var html = '
'; - - return html; + return `
`; } - function buildChapterCards(item, chapters, options) { - + export function buildChapterCards(item, chapters, options) { if (options.parentContainer) { // Abort if the container has been disposed if (!document.body.contains(options.parentContainer)) { @@ -121,15 +122,16 @@ define(['datetime', 'imageLoader', 'connectionManager', 'layoutManager', 'browse } } - var html = buildChapterCardsHtml(item, chapters, options); + const html = buildChapterCardsHtml(item, chapters, options); options.itemsContainer.innerHTML = html; imageLoader.lazyChildren(options.itemsContainer); } - return { - buildChapterCards: buildChapterCards - }; +/* eslint-enable indent */ + +export default { + buildChapterCards: buildChapterCards +}; -}); diff --git a/src/components/cardbuilder/peoplecardbuilder.js b/src/components/cardbuilder/peoplecardbuilder.js index 5d34d29e6e..5fc9e8ade5 100644 --- a/src/components/cardbuilder/peoplecardbuilder.js +++ b/src/components/cardbuilder/peoplecardbuilder.js @@ -1,8 +1,13 @@ -define(['cardBuilder'], function (cardBuilder) { - 'use strict'; +/* eslint-disable indent */ - function buildPeopleCards(items, options) { +/** + * Module for building cards from item data. + * @module components/cardBuilder/peoplecardbuilder + */ +import cardBuilder from 'cardBuilder'; + + export function buildPeopleCards(items, options) { options = Object.assign(options || {}, { cardLayout: false, centerText: true, @@ -15,8 +20,8 @@ define(['cardBuilder'], function (cardBuilder) { cardBuilder.buildCards(items, options); } - return { - buildPeopleCards: buildPeopleCards - }; + /* eslint-enable indent */ -}); +export default { + buildPeopleCards: buildPeopleCards +}; diff --git a/src/components/channelmapper/channelmapper.js b/src/components/channelMapper/channelMapper.js similarity index 67% rename from src/components/channelmapper/channelmapper.js rename to src/components/channelMapper/channelMapper.js index 83ae4d09c6..89d085c185 100644 --- a/src/components/channelmapper/channelmapper.js +++ b/src/components/channelMapper/channelMapper.js @@ -1,10 +1,21 @@ -define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'actionsheet', 'emby-input', 'paper-icon-button-light', 'emby-button', 'listViewStyle', 'material-icons', 'formDialogStyle'], function (dom, dialogHelper, loading, connectionManager, globalize, actionsheet) { - 'use strict'; +import dom from 'dom'; +import dialogHelper from 'dialogHelper'; +import loading from 'loading'; +import connectionManager from 'connectionManager'; +import globalize from 'globalize'; +import actionsheet from 'actionsheet'; +import 'emby-input'; +import 'paper-icon-button-light'; +import 'emby-button'; +import 'listViewStyle'; +import 'material-icons'; +import 'formDialogStyle'; - return function (options) { +export default class channelMapper { + constructor(options) { function mapChannel(button, channelId, providerChannelId) { loading.show(); - var providerId = options.providerId; + const providerId = options.providerId; connectionManager.getApiClient(options.serverId).ajax({ type: 'POST', url: ApiClient.getUrl('LiveTv/ChannelMappings'), @@ -14,8 +25,8 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act providerChannelId: providerChannelId }, dataType: 'json' - }).then(function (mapping) { - var listItem = dom.parentWithClass(button, 'listItem'); + }).then(mapping => { + const listItem = dom.parentWithClass(button, 'listItem'); button.setAttribute('data-providerid', mapping.ProviderChannelId); listItem.querySelector('.secondary').innerHTML = getMappingSecondaryName(mapping, currentMappingOptions.ProviderName); loading.hide(); @@ -23,42 +34,42 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act } function onChannelsElementClick(e) { - var btnMap = dom.parentWithClass(e.target, 'btnMap'); + const btnMap = dom.parentWithClass(e.target, 'btnMap'); if (btnMap) { - var channelId = btnMap.getAttribute('data-id'); - var providerChannelId = btnMap.getAttribute('data-providerid'); - var menuItems = currentMappingOptions.ProviderChannels.map(function (m) { + const channelId = btnMap.getAttribute('data-id'); + const providerChannelId = btnMap.getAttribute('data-providerid'); + const menuItems = currentMappingOptions.ProviderChannels.map(m => { return { name: m.Name, id: m.Id, selected: m.Id.toLowerCase() === providerChannelId.toLowerCase() }; - }).sort(function (a, b) { + }).sort((a, b) => { return a.name.localeCompare(b.name); }); actionsheet.show({ positionTo: btnMap, items: menuItems - }).then(function (newChannelId) { + }).then(newChannelId => { mapChannel(btnMap, channelId, newChannelId); }); } } function getChannelMappingOptions(serverId, providerId) { - var apiClient = connectionManager.getApiClient(serverId); + const apiClient = connectionManager.getApiClient(serverId); return apiClient.getJSON(apiClient.getUrl('LiveTv/ChannelMappingOptions', { providerId: providerId })); } function getMappingSecondaryName(mapping, providerName) { - return (mapping.ProviderChannelName || '') + ' - ' + providerName; + return `${mapping.ProviderChannelName || ''} - ${providerName}`; } function getTunerChannelHtml(channel, providerName) { - var html = ''; + let html = ''; html += '
'; html += ''; html += '
'; @@ -73,16 +84,16 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act html += '
'; html += '
'; - html += ''; + html += ``; return html += '
'; } function getEditorHtml() { - var html = ''; - html += '
'; + let html = ''; + html += '
'; html += '
'; html += '
'; - html += '

' + globalize.translate('HeaderChannels') + '

'; + html += `

${globalize.translate('HeaderChannels')}

`; html += '
'; html += '
'; html += ''; @@ -91,30 +102,29 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act } function initEditor(dlg, options) { - getChannelMappingOptions(options.serverId, options.providerId).then(function (result) { + getChannelMappingOptions(options.serverId, options.providerId).then(result => { currentMappingOptions = result; - var channelsElement = dlg.querySelector('.channels'); - channelsElement.innerHTML = result.TunerChannels.map(function (channel) { + const channelsElement = dlg.querySelector('.channels'); + channelsElement.innerHTML = result.TunerChannels.map(channel => { return getTunerChannelHtml(channel, result.ProviderName); }).join(''); channelsElement.addEventListener('click', onChannelsElementClick); }); } - var currentMappingOptions; - var self = this; + let currentMappingOptions; - self.show = function () { - var dialogOptions = { + this.show = () => { + const dialogOptions = { removeOnClose: true }; dialogOptions.size = 'small'; - var dlg = dialogHelper.createDialog(dialogOptions); + const dlg = dialogHelper.createDialog(dialogOptions); dlg.classList.add('formDialog'); dlg.classList.add('ui-body-a'); dlg.classList.add('background-theme-a'); - var html = ''; - var title = globalize.translate('MapChannels'); + let html = ''; + const title = globalize.translate('MapChannels'); html += '
'; html += ''; html += '

'; @@ -124,13 +134,13 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act html += getEditorHtml(); dlg.innerHTML = html; initEditor(dlg, options); - dlg.querySelector('.btnCancel').addEventListener('click', function () { + dlg.querySelector('.btnCancel').addEventListener('click', () => { dialogHelper.close(dlg); }); - return new Promise(function (resolve, reject) { + return new Promise(resolve => { dlg.addEventListener('close', resolve); dialogHelper.open(dlg); }); }; - }; -}); + } +} diff --git a/src/components/collectioneditor/collectioneditor.js b/src/components/collectionEditor/collectionEditor.js similarity index 51% rename from src/components/collectioneditor/collectioneditor.js rename to src/components/collectionEditor/collectionEditor.js index 46b0640305..a115b86a8f 100644 --- a/src/components/collectioneditor/collectioneditor.js +++ b/src/components/collectionEditor/collectionEditor.js @@ -1,16 +1,31 @@ -define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectionManager', 'appRouter', 'globalize', 'emby-checkbox', 'emby-input', 'paper-icon-button-light', 'emby-select', 'material-icons', 'css!./../formdialog', 'emby-button', 'flexStyles'], function (dom, dialogHelper, loading, appHost, layoutManager, connectionManager, appRouter, globalize) { - 'use strict'; +import dom from 'dom'; +import dialogHelper from 'dialogHelper'; +import loading from 'loading'; +import layoutManager from 'layoutManager'; +import connectionManager from 'connectionManager'; +import appRouter from 'appRouter'; +import globalize from 'globalize'; +import 'emby-checkbox'; +import 'emby-input'; +import 'paper-icon-button-light'; +import 'emby-select'; +import 'material-icons'; +import 'css!./../formdialog'; +import 'emby-button'; +import 'flexStyles'; - var currentServerId; +/* eslint-disable indent */ + + let currentServerId; function onSubmit(e) { loading.show(); - var panel = dom.parentWithClass(this, 'dialog'); + const panel = dom.parentWithClass(this, 'dialog'); - var collectionId = panel.querySelector('#selectCollectionToAddTo').value; + const collectionId = panel.querySelector('#selectCollectionToAddTo').value; - var apiClient = connectionManager.getApiClient(currentServerId); + const apiClient = connectionManager.getApiClient(currentServerId); if (collectionId) { addToCollection(apiClient, panel, collectionId); @@ -23,8 +38,7 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function createCollection(apiClient, dlg) { - - var url = apiClient.getUrl('Collections', { + const url = apiClient.getUrl('Collections', { Name: dlg.querySelector('#txtNewCollectionName').value, IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked, @@ -36,27 +50,23 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio url: url, dataType: 'json' - }).then(function (result) { - + }).then(result => { loading.hide(); - var id = result.Id; + const id = result.Id; dlg.submitted = true; dialogHelper.close(dlg); redirectToCollection(apiClient, id); - }); } function redirectToCollection(apiClient, id) { - appRouter.showItem(id, apiClient.serverId()); } function addToCollection(apiClient, dlg, id) { - - var url = apiClient.getUrl('Collections/' + id + '/Items', { + const url = apiClient.getUrl(`Collections/${id}/Items`, { Ids: dlg.querySelector('.fldSelectedItemIds').value || '' }); @@ -65,14 +75,13 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio type: 'POST', url: url - }).then(function () { - + }).then(() => { loading.hide(); dlg.submitted = true; dialogHelper.close(dlg); - require(['toast'], function (toast) { + import('toast').then(({default: toast}) => { toast(globalize.translate('MessageItemsAdded')); }); }); @@ -83,14 +92,13 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function populateCollections(panel) { - loading.show(); - var select = panel.querySelector('#selectCollectionToAddTo'); + const select = panel.querySelector('#selectCollectionToAddTo'); panel.querySelector('.newCollectionInfo').classList.add('hide'); - var options = { + const options = { Recursive: true, IncludeItemTypes: 'BoxSet', @@ -98,16 +106,14 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio EnableTotalRecordCount: false }; - var apiClient = connectionManager.getApiClient(currentServerId); - apiClient.getItems(apiClient.getCurrentUserId(), options).then(function (result) { + const apiClient = connectionManager.getApiClient(currentServerId); + apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => { + let html = ''; - var html = ''; + html += ``; - html += ''; - - html += result.Items.map(function (i) { - - return ''; + html += result.Items.map(i => { + return ``; }); select.innerHTML = html; @@ -119,8 +125,7 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function getEditorHtml() { - - var html = ''; + let html = ''; html += '
'; html += '
'; @@ -134,27 +139,27 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio html += '
'; html += '
'; html += '
'; - html += ''; + html += ``; html += '
'; html += '
'; html += '
'; html += '
'; - html += ''; - html += '
' + globalize.translate('NewCollectionNameExample') + '
'; + html += ``; + html += `
${globalize.translate('NewCollectionNameExample')}
`; html += '
'; html += ''; // newCollectionInfo html += '
'; html += '
'; - html += ''; + html += ``; html += '
'; html += ''; @@ -167,7 +172,6 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function initEditor(content, items) { - content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () { if (this.value) { content.querySelector('.newCollectionInfo').classList.add('hide'); @@ -188,7 +192,7 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } else { content.querySelector('.fldSelectCollection').classList.add('hide'); - var selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo'); + const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo'); selectCollectionToAddTo.innerHTML = ''; selectCollectionToAddTo.value = ''; triggerChange(selectCollectionToAddTo); @@ -196,79 +200,70 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function centerFocus(elem, horiz, on) { - require(['scrollHelper'], function (scrollHelper) { - var fn = on ? 'on' : 'off'; + import('scrollHelper').then(scrollHelper => { + const fn = on ? 'on' : 'off'; scrollHelper.centerFocus[fn](elem, horiz); }); } - function CollectionEditor() { + export class showEditor { + constructor(options) { + const items = options.items || {}; + currentServerId = options.serverId; - } - - CollectionEditor.prototype.show = function (options) { - - var items = options.items || {}; - currentServerId = options.serverId; - - var dialogOptions = { - removeOnClose: true, - scrollY: false - }; - - if (layoutManager.tv) { - dialogOptions.size = 'fullscreen'; - } else { - dialogOptions.size = 'small'; - } - - var dlg = dialogHelper.createDialog(dialogOptions); - - dlg.classList.add('formDialog'); - - var html = ''; - var title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection'); - - html += '
'; - html += ''; - html += '

'; - html += title; - html += '

'; - - if (appHost.supports('externallinks')) { - html += '' + globalize.translate('Help') + ''; - } - - html += '
'; - - html += getEditorHtml(); - - dlg.innerHTML = html; - - initEditor(dlg, items); - - dlg.querySelector('.btnCancel').addEventListener('click', function () { - - dialogHelper.close(dlg); - }); - - if (layoutManager.tv) { - centerFocus(dlg.querySelector('.formDialogContent'), false, true); - } - - return dialogHelper.open(dlg).then(function () { + const dialogOptions = { + removeOnClose: true, + scrollY: false + }; if (layoutManager.tv) { - centerFocus(dlg.querySelector('.formDialogContent'), false, false); + dialogOptions.size = 'fullscreen'; + } else { + dialogOptions.size = 'small'; } - if (dlg.submitted) { - return Promise.resolve(); + const dlg = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('formDialog'); + + let html = ''; + const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection'); + + html += '
'; + html += ''; + html += '

'; + html += title; + html += '

'; + + html += '
'; + + html += getEditorHtml(); + + dlg.innerHTML = html; + + initEditor(dlg, items); + + dlg.querySelector('.btnCancel').addEventListener('click', () => { + dialogHelper.close(dlg); + }); + + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, true); } - return Promise.reject(); - }); - }; + return dialogHelper.open(dlg).then(() => { + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, false); + } - return CollectionEditor; -}); + if (dlg.submitted) { + return Promise.resolve(); + } + + return Promise.reject(); + }); + } + } + +/* eslint-enable indent */ +export default showEditor; diff --git a/src/components/confirm/confirm.js b/src/components/confirm/confirm.js index 517d6fa9be..0670816a53 100644 --- a/src/components/confirm/confirm.js +++ b/src/components/confirm/confirm.js @@ -1,13 +1,16 @@ -define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) { - 'use strict'; +import browser from 'browser'; +import dialog from 'dialog'; +import globalize from 'globalize'; +/* eslint-disable indent */ +export default (() => { function replaceAll(str, find, replace) { return str.split(find).join(replace); } if (browser.tv && window.confirm) { // Use the native confirm dialog - return function (options) { + return options => { if (typeof options === 'string') { options = { title: '', @@ -15,8 +18,8 @@ define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) }; } - var text = replaceAll(options.text || '', '
', '\n'); - var result = confirm(text); + const text = replaceAll(options.text || '', '
', '\n'); + const result = confirm(text); if (result) { return Promise.resolve(); @@ -26,8 +29,8 @@ define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) }; } else { // Use our own dialog - return function (text, title) { - var options; + return (text, title) => { + let options; if (typeof text === 'string') { options = { title: title, @@ -37,7 +40,7 @@ define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) options = text; } - var items = []; + const items = []; items.push({ name: options.cancelText || globalize.translate('ButtonCancel'), @@ -53,7 +56,7 @@ define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) options.buttons = items; - return dialog(options).then(function (result) { + return dialog.show(options).then(result => { if (result === 'ok') { return Promise.resolve(); } @@ -62,4 +65,5 @@ define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) }); }; } -}); +})(); +/* eslint-enable indent */ diff --git a/src/components/deletehelper.js b/src/components/deletehelper.js deleted file mode 100644 index 2212fd4437..0000000000 --- a/src/components/deletehelper.js +++ /dev/null @@ -1,57 +0,0 @@ -define(['connectionManager', 'confirm', 'appRouter', 'globalize'], function (connectionManager, confirm, appRouter, globalize) { - 'use strict'; - - function alertText(options) { - - return new Promise(function (resolve, reject) { - - require(['alert'], function (alert) { - alert(options).then(resolve, resolve); - }); - }); - } - - function deleteItem(options) { - - var item = options.item; - var itemId = item.Id; - var parentId = item.SeasonId || item.SeriesId || item.ParentId; - var serverId = item.ServerId; - - var msg = globalize.translate('ConfirmDeleteItem'); - var title = globalize.translate('HeaderDeleteItem'); - var apiClient = connectionManager.getApiClient(item.ServerId); - - return confirm({ - - title: title, - text: msg, - confirmText: globalize.translate('Delete'), - primary: 'delete' - - }).then(function () { - - return apiClient.deleteItem(itemId).then(function () { - - if (options.navigate) { - if (parentId) { - appRouter.showItem(parentId, serverId); - } else { - appRouter.goHome(); - } - } - }, function (err) { - - var result = function () { - return Promise.reject(err); - }; - - return alertText(globalize.translate('ErrorDeletingItem')).then(result, result); - }); - }); - } - - return { - deleteItem: deleteItem - }; -}); diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index cfb5821b38..1b13900d85 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -1,20 +1,30 @@ -define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 'require', 'material-icons', 'emby-button', 'paper-icon-button-light', 'emby-input', 'formDialogStyle', 'flexStyles'], function (dialogHelper, dom, layoutManager, scrollHelper, globalize, require) { - 'use strict'; +import dialogHelper from 'dialogHelper'; +import dom from 'dom'; +import layoutManager from 'layoutManager'; +import scrollHelper from 'scrollHelper'; +import globalize from 'globalize'; +import 'material-icons'; +import 'emby-button'; +import 'paper-icon-button-light'; +import 'emby-input'; +import 'formDialogStyle'; +import 'flexStyles'; + +/* eslint-disable indent */ function showDialog(options, template) { - - var dialogOptions = { + const dialogOptions = { removeOnClose: true, scrollY: false }; - var enableTvLayout = layoutManager.tv; + const enableTvLayout = layoutManager.tv; if (enableTvLayout) { dialogOptions.size = 'fullscreen'; } - var dlg = dialogHelper.createDialog(dialogOptions); + const dlg = dialogHelper.createDialog(dialogOptions); dlg.classList.add('formDialog'); @@ -22,7 +32,7 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're dlg.classList.add('align-items-center'); dlg.classList.add('justify-content-center'); - var formDialogContent = dlg.querySelector('.formDialogContent'); + const formDialogContent = dlg.querySelector('.formDialogContent'); formDialogContent.classList.add('no-grow'); if (enableTvLayout) { @@ -30,41 +40,36 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're formDialogContent.style['max-height'] = '60%'; scrollHelper.centerFocus.on(formDialogContent, false); } else { - formDialogContent.style.maxWidth = (Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)) + 'px'; + formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`; dlg.classList.add('dialog-fullscreen-lowres'); } - //dlg.querySelector('.btnCancel').addEventListener('click', function (e) { - // dialogHelper.close(dlg); - //}); - if (options.title) { dlg.querySelector('.formDialogHeaderTitle').innerHTML = options.title || ''; } else { dlg.querySelector('.formDialogHeaderTitle').classList.add('hide'); } - var displayText = options.html || options.text || ''; + const displayText = options.html || options.text || ''; dlg.querySelector('.text').innerHTML = displayText; if (!displayText) { dlg.querySelector('.dialogContentInner').classList.add('hide'); } - var i; - var length; - var html = ''; - var hasDescriptions = false; + let i; + let length; + let html = ''; + let hasDescriptions = false; for (i = 0, length = options.buttons.length; i < length; i++) { + const item = options.buttons[i]; + const autoFocus = i === 0 ? ' autofocus' : ''; - var item = options.buttons[i]; - var autoFocus = i === 0 ? ' autofocus' : ''; - - var buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize'; + let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize'; if (item.type) { - buttonClass += ' button-' + item.type; + buttonClass += ` button-${item.type}`; } if (item.description) { @@ -75,10 +80,10 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom'; } - html += ''; + html += ``; if (item.description) { - html += '
' + item.description + '
'; + html += `
${item.description}
`; } } @@ -88,19 +93,18 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical'); } - var dialogResult; + let dialogResult; function onButtonClick() { dialogResult = this.getAttribute('data-id'); dialogHelper.close(dlg); } - var buttons = dlg.querySelectorAll('.btnOption'); + const buttons = dlg.querySelectorAll('.btnOption'); for (i = 0, length = buttons.length; i < length; i++) { buttons[i].addEventListener('click', onButtonClick); } - return dialogHelper.open(dlg).then(function () { - + return dialogHelper.open(dlg).then(() => { if (enableTvLayout) { scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false); } @@ -113,9 +117,8 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're }); } - return function (text, title) { - - var options; + export async function show(text, title) { + let options; if (typeof text === 'string') { options = { title: title, @@ -125,10 +128,13 @@ define(['dialogHelper', 'dom', 'layoutManager', 'scrollHelper', 'globalize', 're options = text; } - return new Promise(function (resolve, reject) { - require(['text!./dialog.template.html'], function (template) { - showDialog(options, template).then(resolve, reject); - }); + const { default: template } = await import('text!./dialog.template.html'); + return new Promise((resolve, reject) => { + showDialog(options, template).then(resolve, reject); }); - }; -}); + } + +/* eslint-enable indent */ +export default { + show: show +}; diff --git a/src/components/dialog/dialog.template.html b/src/components/dialog/dialog.template.html index bee0ef7f73..6d4310c0f4 100644 --- a/src/components/dialog/dialog.template.html +++ b/src/components/dialog/dialog.template.html @@ -4,12 +4,8 @@
- -
- -
+
-
-
+
diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js index 8b08cde678..1f11d8a195 100644 --- a/src/components/dialogHelper/dialogHelper.js +++ b/src/components/dialogHelper/dialogHelper.js @@ -1,10 +1,17 @@ -define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', 'dom', 'css!./dialoghelper.css', 'scrollStyles'], function (appRouter, focusManager, browser, layoutManager, inputManager, dom) { - 'use strict'; +import appRouter from 'appRouter'; +import focusManager from 'focusManager'; +import browser from 'browser'; +import layoutManager from 'layoutManager'; +import inputManager from 'inputManager'; +import dom from 'dom'; +import 'css!./dialoghelper.css'; +import 'scrollStyles'; - var globalOnOpenCallback; +/* eslint-disable indent */ + + let globalOnOpenCallback; function enableAnimation() { - // too slow if (browser.tv) { return false; @@ -14,7 +21,6 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function removeCenterFocus(dlg) { - if (layoutManager.tv) { if (dlg.classList.contains('scrollX')) { centerFocus(dlg, true, false); @@ -25,9 +31,8 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function tryRemoveElement(elem) { - var parentNode = elem.parentNode; + const parentNode = elem.parentNode; if (parentNode) { - // Seeing crashes in edge webview try { parentNode.removeChild(elem); @@ -38,15 +43,13 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function DialogHashHandler(dlg, hash, resolve) { - - var self = this; + const self = this; self.originalUrl = window.location.href; - var activeElement = document.activeElement; - var removeScrollLockOnClose = false; + const activeElement = document.activeElement; + let removeScrollLockOnClose = false; function onHashChange(e) { - - var isBack = self.originalUrl === window.location.href; + const isBack = self.originalUrl === window.location.href; if (isBack || !isOpened(dlg)) { window.removeEventListener('popstate', onHashChange); @@ -59,7 +62,6 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function onBackCommand(e) { - if (e.detail.command === 'back') { self.closedByBack = true; e.preventDefault(); @@ -69,7 +71,6 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function onDialogClosed() { - if (!isHistoryEnabled(dlg)) { inputManager.off(dlg, onBackCommand); } @@ -84,7 +85,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } if (!self.closedByBack && isHistoryEnabled(dlg)) { - var state = history.state || {}; + const state = history.state || {}; if (state.dialogId === hash) { history.back(); } @@ -97,7 +98,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', if (dlg.getAttribute('data-removeonclose') !== 'false') { removeCenterFocus(dlg); - var dialogContainer = dlg.dialogContainer; + const dialogContainer = dlg.dialogContainer; if (dialogContainer) { tryRemoveElement(dialogContainer); dlg.dialogContainer = null; @@ -108,7 +109,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', //resolve(); // if we just called history.back(), then use a timeout to allow the history events to fire first - setTimeout(function () { + setTimeout(() => { resolve({ element: dlg, closedByBack: self.closedByBack @@ -118,7 +119,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', dlg.addEventListener('close', onDialogClosed); - var center = !dlg.classList.contains('dialog-fixedSize'); + const center = !dlg.classList.contains('dialog-fixedSize'); if (center) { dlg.classList.add('centeredDialog'); } @@ -141,7 +142,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', animateDialogOpen(dlg); if (isHistoryEnabled(dlg)) { - appRouter.pushState({ dialogId: hash }, 'Dialog', '#' + hash); + appRouter.pushState({ dialogId: hash }, 'Dialog', `#${hash}`); window.addEventListener('popstate', onHashChange); } else { @@ -150,11 +151,10 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function addBackdropOverlay(dlg) { - - var backdrop = document.createElement('div'); + const backdrop = document.createElement('div'); backdrop.classList.add('dialogBackdrop'); - var backdropParent = dlg.dialogContainer || dlg; + const backdropParent = dlg.dialogContainer || dlg; backdropParent.parentNode.insertBefore(backdrop, backdropParent); dlg.backdrop = backdrop; @@ -162,7 +162,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', void backdrop.offsetWidth; backdrop.classList.add('dialogBackdropOpened'); - dom.addEventListener((dlg.dialogContainer || backdrop), 'click', function (e) { + dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => { if (e.target === dlg.dialogContainer) { close(dlg); } @@ -170,7 +170,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', passive: true }); - dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', function (e) { + dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => { if (e.target === dlg.dialogContainer) { // Close the application dialog menu close(dlg); @@ -184,37 +184,33 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', return dlg.getAttribute('data-history') === 'true'; } - function open(dlg) { - + export function open(dlg) { if (globalOnOpenCallback) { globalOnOpenCallback(dlg); } - var parent = dlg.parentNode; + const parent = dlg.parentNode; if (parent) { parent.removeChild(dlg); } - var dialogContainer = document.createElement('div'); + const dialogContainer = document.createElement('div'); dialogContainer.classList.add('dialogContainer'); dialogContainer.appendChild(dlg); dlg.dialogContainer = dialogContainer; document.body.appendChild(dialogContainer); - return new Promise(function (resolve, reject) { - - new DialogHashHandler(dlg, 'dlg' + new Date().getTime(), resolve); + return new Promise((resolve, reject) => { + new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve); }); } function isOpened(dlg) { - //return dlg.opened; return !dlg.classList.contains('hide'); } - function close(dlg) { - + export function close(dlg) { if (isOpened(dlg)) { if (isHistoryEnabled(dlg)) { history.back(); @@ -225,15 +221,13 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function closeDialog(dlg) { - if (!dlg.classList.contains('hide')) { - dlg.dispatchEvent(new CustomEvent('closing', { bubbles: false, cancelable: false })); - var onAnimationFinish = function () { + const onAnimationFinish = () => { focusManager.popScope(dlg); dlg.classList.add('hide'); @@ -248,8 +242,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function animateDialogOpen(dlg) { - - var onAnimationFinish = function () { + const onAnimationFinish = () => { focusManager.pushScope(dlg); if (dlg.getAttribute('data-autofocus') === 'true') { @@ -263,8 +256,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', }; if (enableAnimation()) { - - var onFinish = function () { + const onFinish = () => { dom.removeEventListener(dlg, dom.whichAnimationEvent(), onFinish, { once: true }); @@ -280,27 +272,24 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function animateDialogClose(dlg, onAnimationFinish) { - if (enableAnimation()) { - - var animated = true; + let animated = true; switch (dlg.animationConfig.exit.name) { - case 'fadeout': - dlg.style.animation = 'fadeout ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`; break; case 'scaledown': - dlg.style.animation = 'scaledown ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`; break; case 'slidedown': - dlg.style.animation = 'slidedown ' + dlg.animationConfig.exit.timing.duration + 'ms ease-out normal both'; + dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`; break; default: animated = false; break; } - var onFinish = function () { + const onFinish = () => { dom.removeEventListener(dlg, dom.whichAnimationEvent(), onFinish, { once: true }); @@ -318,10 +307,9 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', onAnimationFinish(); } - var supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style; + const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style; function shouldLockDocumentScroll(options) { - if (supportsOverscrollBehavior && (options.size || !browser.touch)) { return false; } @@ -342,8 +330,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function removeBackdrop(dlg) { - - var backdrop = dlg.backdrop; + const backdrop = dlg.backdrop; if (!backdrop) { return; @@ -351,12 +338,11 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', dlg.backdrop = null; - var onAnimationFinish = function () { + const onAnimationFinish = () => { tryRemoveElement(backdrop); }; if (enableAnimation()) { - backdrop.classList.remove('dialogBackdropOpened'); // this is not firing animatonend @@ -368,20 +354,19 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } function centerFocus(elem, horiz, on) { - require(['scrollHelper'], function (scrollHelper) { - var fn = on ? 'on' : 'off'; + import('scrollHelper').then(scrollHelper => { + const fn = on ? 'on' : 'off'; scrollHelper.centerFocus[fn](elem, horiz); }); } - function createDialog(options) { - + export function createDialog(options) { options = options || {}; // If there's no native dialog support, use a plain div // Also not working well in samsung tizen browser, content inside not clickable // Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog - var dlg = document.createElement('div'); + const dlg = document.createElement('div'); dlg.classList.add('focuscontainer'); dlg.classList.add('hide'); @@ -406,17 +391,17 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', dlg.setAttribute('data-autofocus', 'true'); } - var defaultEntryAnimation; - var defaultExitAnimation; + let defaultEntryAnimation; + let defaultExitAnimation; defaultEntryAnimation = 'scaleup'; defaultExitAnimation = 'scaledown'; - var entryAnimation = options.entryAnimation || defaultEntryAnimation; - var exitAnimation = options.exitAnimation || defaultExitAnimation; + const entryAnimation = options.entryAnimation || defaultEntryAnimation; + const exitAnimation = options.exitAnimation || defaultExitAnimation; // If it's not fullscreen then lower the default animation speed to make it open really fast - var entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280); - var exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220); + const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280); + const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220); dlg.animationConfig = { // scale up @@ -461,24 +446,22 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', if (options.size) { dlg.classList.add('dialog-fixedSize'); - dlg.classList.add('dialog-' + options.size); + dlg.classList.add(`dialog-${options.size}`); } if (enableAnimation()) { - switch (dlg.animationConfig.entry.name) { - case 'fadein': - dlg.style.animation = 'fadein ' + entryAnimationDuration + 'ms ease-out normal'; + dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`; break; case 'scaleup': - dlg.style.animation = 'scaleup ' + entryAnimationDuration + 'ms ease-out normal both'; + dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`; break; case 'slideup': - dlg.style.animation = 'slideup ' + entryAnimationDuration + 'ms ease-out normal'; + dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`; break; case 'slidedown': - dlg.style.animation = 'slidedown ' + entryAnimationDuration + 'ms ease-out normal'; + dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`; break; default: break; @@ -488,12 +471,15 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', return dlg; } - return { - open: open, - close: close, - createDialog: createDialog, - setOnOpen: function (val) { - globalOnOpenCallback = val; - } - }; -}); + export function setOnOpen(val) { + globalOnOpenCallback = val; + } + +/* eslint-enable indent */ + +export default { + open: open, + close: close, + createDialog: createDialog, + setOnOpen: setOnOpen +}; diff --git a/src/components/dialogHelper/dialoghelper.css b/src/components/dialogHelper/dialoghelper.css index df2adf075f..98cfef1c5d 100644 --- a/src/components/dialogHelper/dialoghelper.css +++ b/src/components/dialogHelper/dialoghelper.css @@ -126,25 +126,10 @@ } @media all and (min-width: 80em) and (min-height: 45em) { - .dialog-medium { - width: 80%; - height: 80%; - } - - .dialog-medium-tall { - width: 80%; - height: 90%; - } - .dialog-small { width: 60%; height: 80%; } - - .dialog-fullscreen-border { - width: 90%; - height: 90%; - } } .noScroll { diff --git a/src/components/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js index 7f13f89ef5..41744bdcbb 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -1,9 +1,19 @@ -define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-input', 'paper-icon-button-light', 'css!./directorybrowser', 'formDialogStyle', 'emby-button'], function(loading, dialogHelper, dom, globalize) { - 'use strict'; +import loading from 'loading'; +import dialogHelper from 'dialogHelper'; +import dom from 'dom'; +import globalize from 'globalize'; +import 'listViewStyle'; +import 'emby-input'; +import 'paper-icon-button-light'; +import 'css!./directorybrowser'; +import 'formDialogStyle'; +import 'emby-button'; + +/* eslint-disable indent */ function getSystemInfo() { return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then( - function(info) { + info => { systemInfo = info; return info; } @@ -21,9 +31,9 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in loading.show(); - var promises = []; + const promises = []; - if ('Network' === path) { + if (path === 'Network') { promises.push(ApiClient.getNetworkDevices()); } else { if (path) { @@ -35,10 +45,10 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } Promise.all(promises).then( - function(responses) { - var folders = responses[0]; - var parentPath = responses[1] || ''; - var html = ''; + responses => { + const folders = responses[0]; + const parentPath = responses[1] || ''; + let html = ''; page.querySelector('.results').scrollTop = 0; page.querySelector('#txtDirectoryPickerPath').value = path || ''; @@ -46,9 +56,9 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in if (path) { html += getItem('lnkPath lnkDirectory', '', parentPath, '...'); } - for (var i = 0, length = folders.length; i < length; i++) { - var folder = folders[i]; - var cssClass = 'File' === folder.Type ? 'lnkPath lnkFile' : 'lnkPath lnkDirectory'; + for (let i = 0, length = folders.length; i < length; i++) { + const folder = folders[i]; + const cssClass = folder.Type === 'File' ? 'lnkPath lnkFile' : 'lnkPath lnkDirectory'; html += getItem(cssClass, folder.Type, folder.Path, folder.Name); } @@ -58,7 +68,7 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in page.querySelector('.results').innerHTML = html; loading.hide(); - }, function() { + }, () => { if (updatePathOnError) { page.querySelector('#txtDirectoryPickerPath').value = ''; page.querySelector('.results').innerHTML = ''; @@ -69,8 +79,8 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } function getItem(cssClass, type, path, name) { - var html = ''; - html += '
'; + let html = ''; + html += `
`; html += '
'; html += '
'; html += name; @@ -82,20 +92,19 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } function getEditorHtml(options, systemInfo) { - var html = ''; + let html = ''; html += '
'; html += '
'; if (!options.pathReadOnly) { - var instruction = options.instruction ? options.instruction + '

' : ''; + const instruction = options.instruction ? `${options.instruction}

` : ''; html += '
'; html += instruction; - html += globalize.translate('MessageDirectoryPickerInstruction', '\\\\server', '\\\\192.168.1.101'); - if ('bsd' === systemInfo.OperatingSystem.toLowerCase()) { + if (systemInfo.OperatingSystem.toLowerCase() === 'bsd') { html += '
'; html += '
'; html += globalize.translate('MessageDirectoryPickerBSDInstruction'); html += '
'; - } else if ('linux' === systemInfo.OperatingSystem.toLowerCase()) { + } else if (systemInfo.OperatingSystem.toLowerCase() === 'linux') { html += '
'; html += '
'; html += globalize.translate('MessageDirectoryPickerLinuxInstruction'); @@ -106,17 +115,17 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in html += '
'; html += '
'; html += '
'; - var labelKey; + let labelKey; if (options.includeFiles !== true) { labelKey = 'LabelFolder'; } else { labelKey = 'LabelPath'; } - var readOnlyAttribute = options.pathReadOnly ? ' readonly' : ''; - html += ''; + const readOnlyAttribute = options.pathReadOnly ? ' readonly' : ''; + html += ``; html += '
'; if (!readOnlyAttribute) { - html += ''; + html += ``; } html += '
'; if (!readOnlyAttribute) { @@ -124,14 +133,14 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } if (options.enableNetworkSharePath) { html += '
'; - html += ''; + html += ``; html += '
'; - html += globalize.translate('LabelOptionalNetworkPathHelp'); + html += globalize.translate('LabelOptionalNetworkPathHelp', '\\\\server', '\\\\192.168.1.101'); html += '
'; html += '
'; } html += '
'; - html += ''; + html += ``; html += '
'; html += ''; html += '
'; @@ -148,7 +157,7 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } function alertTextWithOptions(options) { - require(['alert'], function(alert) { + import('alert').then(({default: alert}) => { alert(options); }); } @@ -161,7 +170,7 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in ValidateWriteable: validateWriteable, Path: path } - }).catch(function(response) { + }).catch(response => { if (response) { if (response.status === 404) { alertText(globalize.translate('PathNotFound')); @@ -181,10 +190,10 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } function initEditor(content, options, fileOptions) { - content.addEventListener('click', function(e) { - var lnkPath = dom.parentWithClass(e.target, 'lnkPath'); + content.addEventListener('click', e => { + const lnkPath = dom.parentWithClass(e.target, 'lnkPath'); if (lnkPath) { - var path = lnkPath.getAttribute('data-path'); + const path = lnkPath.getAttribute('data-path'); if (lnkPath.classList.contains('lnkFile')) { content.querySelector('#txtDirectoryPickerPath').value = path; } else { @@ -193,25 +202,25 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } }); - content.addEventListener('click', function(e) { + content.addEventListener('click', e => { if (dom.parentWithClass(e.target, 'btnRefreshDirectories')) { - var path = content.querySelector('#txtDirectoryPickerPath').value; + const path = content.querySelector('#txtDirectoryPickerPath').value; refreshDirectoryBrowser(content, path, fileOptions); } }); - content.addEventListener('change', function(e) { - var txtDirectoryPickerPath = dom.parentWithTag(e.target, 'INPUT'); - if (txtDirectoryPickerPath && 'txtDirectoryPickerPath' === txtDirectoryPickerPath.id) { + content.addEventListener('change', e => { + const txtDirectoryPickerPath = dom.parentWithTag(e.target, 'INPUT'); + if (txtDirectoryPickerPath && txtDirectoryPickerPath.id === 'txtDirectoryPickerPath') { refreshDirectoryBrowser(content, txtDirectoryPickerPath.value, fileOptions); } }); content.querySelector('form').addEventListener('submit', function(e) { if (options.callback) { - var networkSharePath = this.querySelector('#txtNetworkPath'); + let networkSharePath = this.querySelector('#txtNetworkPath'); networkSharePath = networkSharePath ? networkSharePath.value : null; - var path = this.querySelector('#txtDirectoryPickerPath').value; + const path = this.querySelector('#txtDirectoryPickerPath').value; validatePath(path, options.validateWriteable, ApiClient).then(options.callback(path, networkSharePath)); } e.preventDefault(); @@ -225,77 +234,79 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in return Promise.resolve(options.path); } else { return ApiClient.getJSON(ApiClient.getUrl('Environment/DefaultDirectoryBrowser')).then( - function(result) { + result => { return result.Path || ''; - }, function() { + }, () => { return ''; } ); } } - function directoryBrowser() { - var currentDialog; - var self = this; - self.show = function(options) { - options = options || {}; - var fileOptions = { - includeDirectories: true - }; - if (options.includeDirectories != null) { - fileOptions.includeDirectories = options.includeDirectories; - } - if (options.includeFiles != null) { - fileOptions.includeFiles = options.includeFiles; - } - Promise.all([getSystemInfo(), getDefaultPath(options)]).then( - function(responses) { - var systemInfo = responses[0]; - var initialPath = responses[1]; - var dlg = dialogHelper.createDialog({ - size: 'medium-tall', - removeOnClose: true, - scrollY: false - }); - dlg.classList.add('ui-body-a'); - dlg.classList.add('background-theme-a'); - dlg.classList.add('directoryPicker'); - dlg.classList.add('formDialog'); - - var html = ''; - html += '
'; - html += ''; - html += '

'; - html += options.header || globalize.translate('HeaderSelectPath'); - html += '

'; - html += '
'; - html += getEditorHtml(options, systemInfo); - dlg.innerHTML = html; - initEditor(dlg, options, fileOptions); - dlg.addEventListener('close', onDialogClosed); - dialogHelper.open(dlg); - dlg.querySelector('.btnCloseDialog').addEventListener('click', function() { - dialogHelper.close(dlg); - }); - currentDialog = dlg; - dlg.querySelector('#txtDirectoryPickerPath').value = initialPath; - var txtNetworkPath = dlg.querySelector('#txtNetworkPath'); - if (txtNetworkPath) { - txtNetworkPath.value = options.networkSharePath || ''; - } - if (!options.pathReadOnly) { - refreshDirectoryBrowser(dlg, initialPath, fileOptions, true); - } + class directoryBrowser { + constructor() { + let currentDialog; + this.show = options => { + options = options || {}; + const fileOptions = { + includeDirectories: true + }; + if (options.includeDirectories != null) { + fileOptions.includeDirectories = options.includeDirectories; } - ); - }; - self.close = function() { - if (currentDialog) { - dialogHelper.close(currentDialog); - } - }; + if (options.includeFiles != null) { + fileOptions.includeFiles = options.includeFiles; + } + Promise.all([getSystemInfo(), getDefaultPath(options)]).then( + responses => { + const systemInfo = responses[0]; + const initialPath = responses[1]; + const dlg = dialogHelper.createDialog({ + size: 'small', + removeOnClose: true, + scrollY: false + }); + dlg.classList.add('ui-body-a'); + dlg.classList.add('background-theme-a'); + dlg.classList.add('directoryPicker'); + dlg.classList.add('formDialog'); + + let html = ''; + html += '
'; + html += ''; + html += '

'; + html += options.header || globalize.translate('HeaderSelectPath'); + html += '

'; + html += '
'; + html += getEditorHtml(options, systemInfo); + dlg.innerHTML = html; + initEditor(dlg, options, fileOptions); + dlg.addEventListener('close', onDialogClosed); + dialogHelper.open(dlg); + dlg.querySelector('.btnCloseDialog').addEventListener('click', () => { + dialogHelper.close(dlg); + }); + currentDialog = dlg; + dlg.querySelector('#txtDirectoryPickerPath').value = initialPath; + const txtNetworkPath = dlg.querySelector('#txtNetworkPath'); + if (txtNetworkPath) { + txtNetworkPath.value = options.networkSharePath || ''; + } + if (!options.pathReadOnly) { + refreshDirectoryBrowser(dlg, initialPath, fileOptions, true); + } + } + ); + }; + this.close = () => { + if (currentDialog) { + dialogHelper.close(currentDialog); + } + }; + } } - var systemInfo; - return directoryBrowser; -}); + let systemInfo; + +/* eslint-enable indent */ +export default directoryBrowser; diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js new file mode 100644 index 0000000000..ae7647f98b --- /dev/null +++ b/src/components/displaySettings/displaySettings.js @@ -0,0 +1,248 @@ +import browser from 'browser'; +import layoutManager from 'layoutManager'; +import pluginManager from 'pluginManager'; +import appHost from 'apphost'; +import focusManager from 'focusManager'; +import datetime from 'datetime'; +import globalize from 'globalize'; +import loading from 'loading'; +import connectionManager from 'connectionManager'; +import skinManager from 'skinManager'; +import events from 'events'; +import 'emby-select'; +import 'emby-checkbox'; +import 'emby-button'; + +/* eslint-disable indent */ + + function fillThemes(context, userSettings) { + const select = context.querySelector('#selectTheme'); + + skinManager.getThemes().then(themes => { + select.innerHTML = themes.map(t => { + return ``; + }).join(''); + + // get default theme + var defaultTheme = themes.find(theme => { + return theme.default; + }); + + // set the current theme + select.value = userSettings.theme() || defaultTheme.id; + }); + } + + function loadScreensavers(context, userSettings) { + const selectScreensaver = context.querySelector('.selectScreensaver'); + const options = pluginManager.ofType('screensaver').map(plugin => { + return { + name: plugin.name, + value: plugin.id + }; + }); + + options.unshift({ + name: globalize.translate('None'), + value: 'none' + }); + + selectScreensaver.innerHTML = options.map(o => { + return ``; + }).join(''); + + selectScreensaver.value = userSettings.screensaver(); + + if (!selectScreensaver.value) { + // TODO: set the default instead of none + selectScreensaver.value = 'none'; + } + } + + function showOrHideMissingEpisodesField(context) { + if (browser.tizen || browser.web0s) { + context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide'); + return; + } + + context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide'); + } + + function loadForm(context, user, userSettings) { + if (appHost.supports('displaylanguage')) { + context.querySelector('.languageSection').classList.remove('hide'); + } else { + context.querySelector('.languageSection').classList.add('hide'); + } + + if (appHost.supports('displaymode')) { + context.querySelector('.fldDisplayMode').classList.remove('hide'); + } else { + context.querySelector('.fldDisplayMode').classList.add('hide'); + } + + if (appHost.supports('externallinks')) { + context.querySelector('.learnHowToContributeContainer').classList.remove('hide'); + } else { + context.querySelector('.learnHowToContributeContainer').classList.add('hide'); + } + + if (appHost.supports('screensaver')) { + context.querySelector('.selectScreensaverContainer').classList.remove('hide'); + } else { + context.querySelector('.selectScreensaverContainer').classList.add('hide'); + } + + if (datetime.supportsLocalization()) { + context.querySelector('.fldDateTimeLocale').classList.remove('hide'); + } else { + context.querySelector('.fldDateTimeLocale').classList.add('hide'); + } + + if (!browser.tizen && !browser.web0s) { + context.querySelector('.fldBackdrops').classList.remove('hide'); + context.querySelector('.fldThemeSong').classList.remove('hide'); + context.querySelector('.fldThemeVideo').classList.remove('hide'); + } else { + context.querySelector('.fldBackdrops').classList.add('hide'); + context.querySelector('.fldThemeSong').classList.add('hide'); + context.querySelector('.fldThemeVideo').classList.add('hide'); + } + + fillThemes(context, userSettings); + loadScreensavers(context, userSettings); + + context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false; + + context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs(); + context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos(); + context.querySelector('#chkFadein').checked = userSettings.enableFastFadein(); + context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash(); + context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops(); + context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner(); + + context.querySelector('#selectLanguage').value = userSettings.language() || ''; + context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || ''; + + context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize(); + + context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || ''; + + showOrHideMissingEpisodesField(context); + + loading.hide(); + } + + function saveUser(context, user, userSettingsInstance, apiClient) { + user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked; + + if (appHost.supports('displaylanguage')) { + userSettingsInstance.language(context.querySelector('#selectLanguage').value); + } + + userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value); + + userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked); + userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked); + userSettingsInstance.theme(context.querySelector('#selectTheme').value); + userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value); + + userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value); + + userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked); + userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked); + userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked); + userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked); + + if (user.Id === apiClient.getCurrentUserId()) { + skinManager.setTheme(userSettingsInstance.theme()); + } + + layoutManager.setLayout(context.querySelector('.selectLayout').value); + return apiClient.updateUserConfiguration(user.Id, user.Configuration); + } + + function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) { + loading.show(); + + apiClient.getUser(userId).then(user => { + saveUser(context, user, userSettings, apiClient).then(() => { + loading.hide(); + if (enableSaveConfirmation) { + import('toast').then(({default: toast}) => { + toast(globalize.translate('SettingsSaved')); + }); + } + events.trigger(instance, 'saved'); + }, () => { + loading.hide(); + }); + }); + } + + function onSubmit(e) { + const self = this; + const apiClient = connectionManager.getApiClient(self.options.serverId); + const userId = self.options.userId; + const userSettings = self.options.userSettings; + + userSettings.setUserInfo(userId, apiClient).then(() => { + const enableSaveConfirmation = self.options.enableSaveConfirmation; + save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation); + }); + + // Disable default form submission + if (e) { + e.preventDefault(); + } + return false; + } + + async function embed(options, self) { + const { default: template } = await import('text!./displaySettings.template.html'); + options.element.innerHTML = globalize.translateHtml(template, 'core'); + options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self)); + if (options.enableSaveButton) { + options.element.querySelector('.btnSave').classList.remove('hide'); + } + self.loadData(options.autoFocus); + } + + class DisplaySettings { + constructor(options) { + this.options = options; + embed(options, this); + } + + loadData(autoFocus) { + const self = this; + const context = self.options.element; + + loading.show(); + + const userId = self.options.userId; + const apiClient = connectionManager.getApiClient(self.options.serverId); + const userSettings = self.options.userSettings; + + return apiClient.getUser(userId).then(user => { + return userSettings.setUserInfo(userId, apiClient).then(() => { + self.dataLoaded = true; + loadForm(context, user, userSettings); + if (autoFocus) { + focusManager.autoFocus(context); + } + }); + }); + } + + submit() { + onSubmit.call(this); + } + + destroy() { + this.options = null; + } + } + +/* eslint-enable indent */ +export default DisplaySettings; diff --git a/src/components/displaysettings/displaysettings.template.html b/src/components/displaySettings/displaySettings.template.html similarity index 89% rename from src/components/displaysettings/displaysettings.template.html rename to src/components/displaySettings/displaySettings.template.html index 62cb493e82..fdaf8d70f1 100644 --- a/src/components/displaysettings/displaysettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -1,5 +1,4 @@
-

${Display}

@@ -56,7 +55,7 @@
@@ -123,37 +122,41 @@
${LabelPleaseRestart}
-
- -
-
-
- -
-
-
- -
- -
+
${LabelLibraryPageSizeHelp}
-
+
-
${EnableFastImageFadeInHelp}
+
${EnableFasterAnimationsHelp}
+
+ +
+ +
${EnableBlurHashHelp}
+
+ +
+ +
${EnableDetailsBannerHelp}
@@ -180,13 +183,6 @@
${EnableThemeVideosHelp}
-
- -
-

'; html += ''; html += ''; - } else { html += '

' + globalize.translate('HeaderOnNow') + '

'; } @@ -654,7 +647,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la elem.innerHTML = html; - var itemsContainer = elem.querySelector('.itemsContainer'); + const itemsContainer = elem.querySelector('.itemsContainer'); itemsContainer.parentContainer = elem; itemsContainer.fetchData = getOnNowFetchFn(apiClient.serverId()); itemsContainer.getItemsHtml = getOnNowItemsHtml; @@ -664,10 +657,10 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la function getNextUpFetchFn(serverId) { return function () { - var apiClient = connectionManager.getApiClient(serverId); + const apiClient = connectionManager.getApiClient(serverId); return apiClient.getNextUpEpisodes({ Limit: enableScrollX() ? 24 : 15, - Fields: 'PrimaryImageAspectRatio,SeriesInfo,DateCreated,BasicSyncInfo', + Fields: 'PrimaryImageAspectRatio,SeriesInfo,DateCreated,BasicSyncInfo,Path', UserId: apiClient.getCurrentUserId(), ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', @@ -677,7 +670,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la } function getNextUpItemsHtml(items) { - var cardLayout = false; + const cardLayout = false; return cardBuilder.getCardsHtml({ items: items, preferThumb: true, @@ -695,7 +688,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la } function loadNextUp(elem, apiClient, userId) { - var html = ''; + let html = ''; html += '
'; if (!layoutManager.tv) { @@ -727,7 +720,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la elem.classList.add('hide'); elem.innerHTML = html; - var itemsContainer = elem.querySelector('.itemsContainer'); + const itemsContainer = elem.querySelector('.itemsContainer'); itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId()); itemsContainer.getItemsHtml = getNextUpItemsHtml; itemsContainer.parentContainer = elem; @@ -735,7 +728,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la function getLatestRecordingsFetchFn(serverId, activeRecordingsOnly) { return function () { - var apiClient = connectionManager.getApiClient(serverId); + const apiClient = connectionManager.getApiClient(serverId); return apiClient.getLiveTvRecordings({ userId: apiClient.getCurrentUserId(), Limit: enableScrollX() ? 12 : 5, @@ -749,7 +742,6 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la function getLatestRecordingItemsHtml(activeRecordingsOnly) { return function (items) { - var cardLayout = false; return cardBuilder.getCardsHtml({ items: items, shape: enableScrollX() ? 'autooverflow' : 'auto', @@ -774,11 +766,11 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la } function loadLatestLiveTvRecordings(elem, activeRecordingsOnly, apiClient, userId) { - var title = activeRecordingsOnly ? + const title = activeRecordingsOnly ? globalize.translate('HeaderActiveRecordings') : globalize.translate('HeaderLatestRecordings'); - var html = ''; + let html = ''; html += '
'; html += '

' + title + '

'; @@ -799,18 +791,19 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la elem.classList.add('hide'); elem.innerHTML = html; - var itemsContainer = elem.querySelector('.itemsContainer'); + const itemsContainer = elem.querySelector('.itemsContainer'); itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly); itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly); itemsContainer.parentContainer = elem; } - return { - loadLibraryTiles: loadLibraryTiles, - getDefaultSection: getDefaultSection, - loadSections: loadSections, - destroySections: destroySections, - pause: pause, - resume: resume - }; -}); +export default { + loadLibraryTiles: loadLibraryTiles, + getDefaultSection: getDefaultSection, + loadSections: loadSections, + destroySections: destroySections, + pause: pause, + resume: resume +}; + +/* eslint-enable indent */ diff --git a/src/components/htmlMediaHelper.js b/src/components/htmlMediaHelper.js index fb84bc19f0..4095502aae 100644 --- a/src/components/htmlMediaHelper.js +++ b/src/components/htmlMediaHelper.js @@ -1,17 +1,20 @@ -define(['appSettings', 'browser', 'events'], function (appSettings, browser, events) { - 'use strict'; +/* eslint-disable indent */ - function getSavedVolume() { +import appSettings from 'appSettings' ; +import browser from 'browser'; +import events from 'events'; + + export function getSavedVolume() { return appSettings.get('volume') || 1; } - function saveVolume(value) { + export function saveVolume(value) { if (value) { appSettings.set('volume', value); } } - function getCrossOriginValue(mediaSource) { + export function getCrossOriginValue(mediaSource) { if (mediaSource.IsRemote) { return null; } @@ -30,34 +33,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve return false; } - function enableHlsShakaPlayer(item, mediaSource, mediaType) { - /* eslint-disable-next-line compat/compat */ - if (!!window.MediaSource && !!MediaSource.isTypeSupported) { - - if (canPlayNativeHls()) { - - if (browser.edge && mediaType === 'Video') { - return true; - } - - // simple playback should use the native support - if (mediaSource.RunTimeTicks) { - //if (!browser.edge) { - //return false; - //} - } - - //return false; - } - - return true; - } - - return false; - } - - function enableHlsJsPlayer(runTimeTicks, mediaType) { - + export function enableHlsJsPlayer(runTimeTicks, mediaType) { if (window.MediaSource == null) { return false; } @@ -73,7 +49,6 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } if (canPlayNativeHls()) { - // Having trouble with chrome's native support and transcoded music if (browser.android && mediaType === 'Audio') { return true; @@ -98,8 +73,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve var recoverDecodingErrorDate; var recoverSwapAudioCodecDate; - function handleHlsJsMediaError(instance, reject) { - + export function handleHlsJsMediaError(instance, reject) { var hlsPlayer = instance._hlsPlayer; if (!hlsPlayer) { @@ -134,8 +108,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function onErrorInternal(instance, type) { - + export function onErrorInternal(instance, type) { // Needed for video if (instance.destroyCustomTrack) { instance.destroyCustomTrack(instance._mediaElement); @@ -148,7 +121,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve ]); } - function isValidDuration(duration) { + export function isValidDuration(duration) { if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { return true; } @@ -162,13 +135,10 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function seekOnPlaybackStart(instance, element, ticks, onMediaReady) { - + export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) { var seconds = (ticks || 0) / 10000000; if (seconds) { - var src = (instance.currentSrc() || '').toLowerCase(); - // Appending #t=xxx to the query string doesn't seem to work with HLS // For plain video files, not all browsers support it either @@ -194,18 +164,15 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } }; events.map(function (name) { - element.addEventListener(name, onMediaChange); + return element.addEventListener(name, onMediaChange); }); } } } - function applySrc(elem, src, options) { - + export function applySrc(elem, src, options) { if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) { - return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) { - var playlist = new Windows.Media.Playback.MediaPlaybackList(); var source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file); @@ -214,9 +181,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true }); return Promise.resolve(); }); - } else { - elem.src = src; } @@ -224,18 +189,15 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } function onSuccessfulPlay(elem, onErrorFn) { - elem.addEventListener('error', onErrorFn); } - function playWithPromise(elem, onErrorFn) { - + export function playWithPromise(elem, onErrorFn) { try { var promise = elem.play(); if (promise && promise.then) { // Chrome now returns a promise return promise.catch(function (e) { - var errorName = (e.name || '').toLowerCase(); // safari uses aborterror if (errorName === 'notallowederror' || @@ -256,8 +218,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function destroyCastPlayer(instance) { - + export function destroyCastPlayer(instance) { var player = instance._castPlayer; if (player) { try { @@ -270,20 +231,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function destroyShakaPlayer(instance) { - var player = instance._shakaPlayer; - if (player) { - try { - player.destroy(); - } catch (err) { - console.error(err); - } - - instance._shakaPlayer = null; - } - } - - function destroyHlsPlayer(instance) { + export function destroyHlsPlayer(instance) { var player = instance._hlsPlayer; if (player) { try { @@ -296,7 +244,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function destroyFlvPlayer(instance) { + export function destroyFlvPlayer(instance) { var player = instance._flvPlayer; if (player) { try { @@ -311,11 +259,9 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } } - function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) { - + export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) { hls.on(Hls.Events.MANIFEST_PARSED, function () { playWithPromise(elem, onErrorFn).then(resolve, function () { - if (reject) { reject(); reject = null; @@ -324,14 +270,12 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve }); hls.on(Hls.Events.ERROR, function (event, data) { - console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false)); switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: // try to recover network error if (data.response && data.response.code && data.response.code >= 400) { - console.debug('hls.js response error code: ' + data.response.code); // Trigger failure differently depending on whether this is prior to start of playback, or after @@ -345,7 +289,6 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve } return; - } break; @@ -358,7 +301,6 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve case Hls.ErrorTypes.NETWORK_ERROR: if (data.response && data.response.code === 0) { - // This could be a CORS error related to access control response headers console.debug('hls.js response error code: ' + data.response.code); @@ -403,8 +345,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve }); } - function onEndedInternal(instance, elem, onErrorFn) { - + export function onEndedInternal(instance, elem, onErrorFn) { elem.removeEventListener('error', onErrorFn); elem.src = ''; @@ -413,7 +354,6 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve destroyHlsPlayer(instance); destroyFlvPlayer(instance); - destroyShakaPlayer(instance); destroyCastPlayer(instance); var stopInfo = { @@ -427,8 +367,7 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve instance._currentPlayOptions = null; } - function getBufferedRanges(instance, elem) { - + export function getBufferedRanges(instance, elem) { var ranges = []; var seekable = elem.buffered || []; @@ -441,7 +380,6 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve offset = offset || 0; for (var i = 0, length = seekable.length; i < length; i++) { - var start = seekable.start(i); var end = seekable.end(i); @@ -462,23 +400,4 @@ define(['appSettings', 'browser', 'events'], function (appSettings, browser, eve return ranges; } - return { - getSavedVolume: getSavedVolume, - saveVolume: saveVolume, - enableHlsJsPlayer: enableHlsJsPlayer, - enableHlsShakaPlayer: enableHlsShakaPlayer, - handleHlsJsMediaError: handleHlsJsMediaError, - isValidDuration: isValidDuration, - onErrorInternal: onErrorInternal, - seekOnPlaybackStart: seekOnPlaybackStart, - applySrc: applySrc, - playWithPromise: playWithPromise, - destroyHlsPlayer: destroyHlsPlayer, - destroyFlvPlayer: destroyFlvPlayer, - destroyCastPlayer: destroyCastPlayer, - bindEventsToHlsPlayer: bindEventsToHlsPlayer, - onEndedInternal: onEndedInternal, - getCrossOriginValue: getCrossOriginValue, - getBufferedRanges: getBufferedRanges - }; -}); +/* eslint-enable indent */ diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js deleted file mode 100644 index f87fd19462..0000000000 --- a/src/components/htmlvideoplayer/plugin.js +++ /dev/null @@ -1,1858 +0,0 @@ -define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackManager', 'appRouter', 'appSettings', 'connectionManager', 'htmlMediaHelper', 'itemHelper', 'screenfull', 'globalize'], function (browser, require, events, appHost, loading, dom, playbackManager, appRouter, appSettings, connectionManager, htmlMediaHelper, itemHelper, screenfull, globalize) { - 'use strict'; - /* globals cast */ - - var mediaManager; - - function tryRemoveElement(elem) { - var parentNode = elem.parentNode; - if (parentNode) { - - // Seeing crashes in edge webview - try { - parentNode.removeChild(elem); - } catch (err) { - console.error('error removing dialog element: ' + err); - } - } - } - - var _supportsTextTracks; - function supportsTextTracks() { - - if (_supportsTextTracks == null) { - _supportsTextTracks = document.createElement('video').textTracks != null; - } - - // For now, until ready - return _supportsTextTracks; - } - - function supportsCanvas() { - return !!document.createElement('canvas').getContext; - } - - function supportsWebWorkers() { - return !!window.Worker; - } - - function enableNativeTrackSupport(currentSrc, track) { - - if (track) { - if (track.DeliveryMethod === 'Embed') { - return true; - } - } - - if (browser.firefox) { - if ((currentSrc || '').toLowerCase().indexOf('.m3u8') !== -1) { - return false; - } - } - - // subs getting blocked due to CORS - if (browser.chromecast) { - if ((currentSrc || '').toLowerCase().indexOf('.m3u8') !== -1) { - return false; - } - } - - if (browser.ps4) { - return false; - } - - if (browser.web0s) { - return false; - } - - // Edge is randomly not rendering subtitles - if (browser.edge) { - return false; - } - - if (browser.iOS) { - // works in the browser but not the native app - if ((browser.iosVersion || 10) < 10) { - return false; - } - } - - if (track) { - var format = (track.Codec || '').toLowerCase(); - if (format === 'ssa' || format === 'ass') { - return false; - } - } - - return true; - } - - function requireHlsPlayer(callback) { - require(['hlsjs'], function (hls) { - window.Hls = hls; - callback(); - }); - } - - function getMediaStreamAudioTracks(mediaSource) { - return mediaSource.MediaStreams.filter(function (s) { - return s.Type === 'Audio'; - }); - } - - function getMediaStreamTextTracks(mediaSource) { - return mediaSource.MediaStreams.filter(function (s) { - return s.Type === 'Subtitle'; - }); - } - - function zoomIn(elem) { - return new Promise(function (resolve, reject) { - var duration = 240; - elem.style.animation = 'htmlvideoplayer-zoomin ' + duration + 'ms ease-in normal'; - dom.addEventListener(elem, dom.whichAnimationEvent(), resolve, { - once: true - }); - }); - } - - function normalizeTrackEventText(text, useHtml) { - var result = text.replace(/\\N/gi, '\n').replace(/\r/gi, ''); - return useHtml ? result.replace(/\n/gi, '
') : result; - } - - function setTracks(elem, tracks, item, mediaSource) { - - elem.innerHTML = getTracksHtml(tracks, item, mediaSource); - } - - function getTextTrackUrl(track, item, format) { - - if (itemHelper.isLocalItem(item) && track.Path) { - return track.Path; - } - - var url = playbackManager.getSubtitleUrl(track, item.ServerId); - if (format) { - url = url.replace('.vtt', format); - } - - return url; - } - - function getTracksHtml(tracks, item, mediaSource) { - return tracks.map(function (t) { - - if (t.DeliveryMethod !== 'External') { - return ''; - } - - var defaultAttribute = mediaSource.DefaultSubtitleStreamIndex === t.Index ? ' default' : ''; - - var language = t.Language || 'und'; - var label = t.Language || 'und'; - return ''; - - }).join(''); - } - - function getDefaultProfile() { - - return new Promise(function (resolve, reject) { - - require(['browserdeviceprofile'], function (profileBuilder) { - - resolve(profileBuilder({})); - }); - }); - } - - function HtmlVideoPlayer() { - - if (browser.edgeUwp) { - this.name = 'Windows Video Player'; - } else { - this.name = 'Html Video Player'; - } - - this.type = 'mediaplayer'; - this.id = 'htmlvideoplayer'; - - // Let any players created by plugins take priority - this.priority = 1; - - var videoDialog; - - var winJsPlaybackItem; - - var subtitleTrackIndexToSetOnPlaying; - var audioTrackIndexToSetOnPlaying; - - var lastCustomTrackMs = 0; - var currentClock; - var currentSubtitlesOctopus; - var currentAssRenderer; - var customTrackIndex = -1; - - var showTrackOffset; - var currentTrackOffset; - - var videoSubtitlesElem; - var currentTrackEvents; - - var self = this; - - self.currentSrc = function () { - return self._currentSrc; - }; - - self._fetchQueue = 0; - self.isFetching = false; - - function incrementFetchQueue() { - if (self._fetchQueue <= 0) { - self.isFetching = true; - events.trigger(self, 'beginFetch'); - } - - self._fetchQueue++; - } - - function decrementFetchQueue() { - self._fetchQueue--; - - if (self._fetchQueue <= 0) { - self.isFetching = false; - events.trigger(self, 'endFetch'); - } - } - - function updateVideoUrl(streamInfo) { - - var isHls = streamInfo.url.toLowerCase().indexOf('.m3u8') !== -1; - - var mediaSource = streamInfo.mediaSource; - var item = streamInfo.item; - - // Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts - // This will start the transcoding process before actually feeding the video url into the player - // Edit: Also seeing stalls from hls.js - if (mediaSource && item && !mediaSource.RunTimeTicks && isHls && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) { - - var hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8'); - - loading.show(); - - console.debug('prefetching hls playlist: ' + hlsPlaylistUrl); - - return connectionManager.getApiClient(item.ServerId).ajax({ - - type: 'GET', - url: hlsPlaylistUrl - - }).then(function () { - - console.debug('completed prefetching hls playlist: ' + hlsPlaylistUrl); - - loading.hide(); - streamInfo.url = hlsPlaylistUrl; - - return Promise.resolve(); - - }, function () { - - console.error('error prefetching hls playlist: ' + hlsPlaylistUrl); - - loading.hide(); - return Promise.resolve(); - }); - - } else { - return Promise.resolve(); - } - } - - self.play = function (options) { - - if (browser.msie) { - if (options.playMethod === 'Transcode' && !window.MediaSource) { - alert('Playback of this content is not supported in Internet Explorer. For a better experience, try a modern browser such as Microsoft Edge, Google Chrome, Firefox or Opera.'); - return Promise.reject(); - } - } - - self._started = false; - self._timeUpdated = false; - - self._currentTime = null; - - self.resetSubtitleOffset(); - - return createMediaElement(options).then(function (elem) { - - return updateVideoUrl(options, options.mediaSource).then(function () { - return setCurrentSrc(elem, options); - }); - }); - }; - - function setSrcWithFlvJs(instance, elem, options, url) { - - return new Promise(function (resolve, reject) { - - require(['flvjs'], function (flvjs) { - - var flvPlayer = flvjs.createPlayer({ - type: 'flv', - url: url - }, - { - seekType: 'range', - lazyLoad: false - }); - - flvPlayer.attachMediaElement(elem); - flvPlayer.load(); - - flvPlayer.play().then(resolve, reject); - instance._flvPlayer = flvPlayer; - - // This is needed in setCurrentTrackElement - self._currentSrc = url; - }); - }); - } - - function setSrcWithHlsJs(instance, elem, options, url) { - - return new Promise(function (resolve, reject) { - - requireHlsPlayer(function () { - - var hls = new Hls({ - manifestLoadingTimeOut: 20000 - //appendErrorMaxRetry: 6, - //debug: true - }); - hls.loadSource(url); - hls.attachMedia(elem); - - htmlMediaHelper.bindEventsToHlsPlayer(self, hls, elem, onError, resolve, reject); - - self._hlsPlayer = hls; - - // This is needed in setCurrentTrackElement - self._currentSrc = url; - }); - }); - } - - function onShakaError(event) { - - var error = event.detail; - console.error('Error code', error.code, 'object', error); - } - - function setSrcWithShakaPlayer(instance, elem, options, url) { - - return new Promise(function (resolve, reject) { - - require(['shaka'], function () { - /* globals shaka */ - - var player = new shaka.Player(elem); - - //player.configure({ - // abr: { - // enabled: false - // }, - // streaming: { - - // failureCallback: function () { - // alert(2); - // } - // } - //}); - - //shaka.log.setLevel(6); - - // Listen for error events. - player.addEventListener('error', onShakaError); - - // Try to load a manifest. - // This is an asynchronous process. - player.load(url).then(function () { - - // This runs if the asynchronous load is successful. - resolve(); - - }, reject); - - self._shakaPlayer = player; - - // This is needed in setCurrentTrackElement - self._currentSrc = url; - }); - }); - } - - function setCurrentSrcChromecast(instance, elem, options, url) { - - elem.autoplay = true; - - var lrd = new cast.receiver.MediaManager.LoadRequestData(); - lrd.currentTime = (options.playerStartPositionTicks || 0) / 10000000; - lrd.autoplay = true; - lrd.media = new cast.receiver.media.MediaInformation(); - - lrd.media.contentId = url; - lrd.media.contentType = options.mimeType; - lrd.media.streamType = cast.receiver.media.StreamType.OTHER; - lrd.media.customData = options; - - console.debug('loading media url into media manager'); - - try { - mediaManager.load(lrd); - // This is needed in setCurrentTrackElement - self._currentSrc = url; - - return Promise.resolve(); - } catch (err) { - - console.debug('media manager error: ' + err); - return Promise.reject(); - } - } - - // Adapted from : https://github.com/googlecast/CastReferencePlayer/blob/master/player.js - function onMediaManagerLoadMedia(event) { - - if (self._castPlayer) { - self._castPlayer.unload(); // Must unload before starting again. - } - self._castPlayer = null; - - var data = event.data; - - var media = event.data.media || {}; - var url = media.contentId; - var contentType = media.contentType.toLowerCase(); - var options = media.customData; - - var protocol; - var ext = 'm3u8'; - - var mediaElement = self._mediaElement; - - var host = new cast.player.api.Host({ - 'url': url, - 'mediaElement': mediaElement - }); - - if (ext === 'm3u8' || - contentType === 'application/x-mpegurl' || - contentType === 'application/vnd.apple.mpegurl') { - protocol = cast.player.api.CreateHlsStreamingProtocol(host); - } else if (ext === 'mpd' || - contentType === 'application/dash+xml') { - protocol = cast.player.api.CreateDashStreamingProtocol(host); - } else if (url.indexOf('.ism') > -1 || - contentType === 'application/vnd.ms-sstr+xml') { - protocol = cast.player.api.CreateSmoothStreamingProtocol(host); - } - - console.debug('loading playback url: ' + url); - console.debug('content type: ' + contentType); - - host.onError = function (errorCode) { - console.error('fatal Error - ' + errorCode); - }; - - mediaElement.autoplay = false; - - self._castPlayer = new cast.player.api.Player(host); - - self._castPlayer.load(protocol, data.currentTime || 0); - - self._castPlayer.playWhenHaveEnoughData(); - } - - function initMediaManager() { - - mediaManager.defaultOnLoad = mediaManager.onLoad.bind(mediaManager); - mediaManager.onLoad = onMediaManagerLoadMedia.bind(self); - - //mediaManager.defaultOnPlay = mediaManager.onPlay.bind(mediaManager); - //mediaManager.onPlay = function (event) { - // // TODO ??? - // mediaManager.defaultOnPlay(event); - //}; - - mediaManager.defaultOnStop = mediaManager.onStop.bind(mediaManager); - mediaManager.onStop = function (event) { - playbackManager.stop(); - mediaManager.defaultOnStop(event); - }; - } - - function setCurrentSrc(elem, options) { - - elem.removeEventListener('error', onError); - - var val = options.url; - console.debug('playing url: ' + val); - - // Convert to seconds - var seconds = (options.playerStartPositionTicks || 0) / 10000000; - if (seconds) { - val += '#t=' + seconds; - } - - htmlMediaHelper.destroyHlsPlayer(self); - htmlMediaHelper.destroyFlvPlayer(self); - htmlMediaHelper.destroyCastPlayer(self); - - var tracks = getMediaStreamTextTracks(options.mediaSource); - - subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex; - if (subtitleTrackIndexToSetOnPlaying != null && subtitleTrackIndexToSetOnPlaying >= 0) { - var initialSubtitleStream = options.mediaSource.MediaStreams[subtitleTrackIndexToSetOnPlaying]; - if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') { - subtitleTrackIndexToSetOnPlaying = -1; - } - } - - audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex; - - self._currentPlayOptions = options; - - var crossOrigin = htmlMediaHelper.getCrossOriginValue(options.mediaSource); - if (crossOrigin) { - elem.crossOrigin = crossOrigin; - } - - /*if (htmlMediaHelper.enableHlsShakaPlayer(options.item, options.mediaSource, 'Video') && val.indexOf('.m3u8') !== -1) { - - setTracks(elem, tracks, options.item, options.mediaSource); - - return setSrcWithShakaPlayer(self, elem, options, val); - - } else*/ if (browser.chromecast && val.indexOf('.m3u8') !== -1 && options.mediaSource.RunTimeTicks) { - - return setCurrentSrcChromecast(self, elem, options, val); - } else if (htmlMediaHelper.enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && val.indexOf('.m3u8') !== -1) { - return setSrcWithHlsJs(self, elem, options, val); - } else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') { - - return setSrcWithFlvJs(self, elem, options, val); - - } else { - - elem.autoplay = true; - - return htmlMediaHelper.applySrc(elem, val, options).then(function () { - - self._currentSrc = val; - - return htmlMediaHelper.playWithPromise(elem, onError); - }); - } - } - - self.setSubtitleStreamIndex = function (index) { - - setCurrentTrackElement(index); - }; - - self.resetSubtitleOffset = function() { - currentTrackOffset = 0; - showTrackOffset = false; - }; - - self.enableShowingSubtitleOffset = function() { - showTrackOffset = true; - }; - - self.disableShowingSubtitleOffset = function() { - showTrackOffset = false; - }; - - self.isShowingSubtitleOffsetEnabled = function() { - return showTrackOffset; - }; - - function getTextTrack() { - var videoElement = self._mediaElement; - if (videoElement) { - return Array.from(videoElement.textTracks) - .find(function(trackElement) { - // get showing .vtt textTack - return trackElement.mode === 'showing'; - }); - } else { - return null; - } - } - - self.setSubtitleOffset = function(offset) { - - var offsetValue = parseFloat(offset); - - // if .ass currently rendering - if (currentSubtitlesOctopus) { - updateCurrentTrackOffset(offsetValue); - currentSubtitlesOctopus.timeOffset = (self._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue; - } else { - var trackElement = getTextTrack(); - // if .vtt currently rendering - if (trackElement) { - setTextTrackSubtitleOffset(trackElement, offsetValue); - } else if (currentTrackEvents) { - setTrackEventsSubtitleOffset(currentTrackEvents, offsetValue); - } else { - console.debug('No available track, cannot apply offset: ', offsetValue); - } - } - }; - - function updateCurrentTrackOffset(offsetValue) { - - var relativeOffset = offsetValue; - var newTrackOffset = offsetValue; - if (currentTrackOffset) { - relativeOffset -= currentTrackOffset; - } - currentTrackOffset = newTrackOffset; - // relative to currentTrackOffset - return relativeOffset; - } - - function setTextTrackSubtitleOffset(currentTrack, offsetValue) { - - if (currentTrack.cues) { - offsetValue = updateCurrentTrackOffset(offsetValue); - Array.from(currentTrack.cues) - .forEach(function(cue) { - cue.startTime -= offsetValue; - cue.endTime -= offsetValue; - }); - } - } - - function setTrackEventsSubtitleOffset(trackEvents, offsetValue) { - - if (Array.isArray(trackEvents)) { - offsetValue = updateCurrentTrackOffset(offsetValue); - trackEvents.forEach(function(trackEvent) { - trackEvent.StartPositionTicks -= offsetValue; - trackEvent.EndPositionTicks -= offsetValue; - }); - } - } - - self.getSubtitleOffset = function() { - return currentTrackOffset; - }; - - function isAudioStreamSupported(stream, deviceProfile) { - - var codec = (stream.Codec || '').toLowerCase(); - - if (!codec) { - return true; - } - - if (!deviceProfile) { - // This should never happen - return true; - } - - var profiles = deviceProfile.DirectPlayProfiles || []; - - return profiles.filter(function (p) { - - if (p.Type === 'Video') { - - if (!p.AudioCodec) { - return true; - } - - return p.AudioCodec.toLowerCase().indexOf(codec) !== -1; - } - - return false; - - }).length > 0; - } - - function getSupportedAudioStreams() { - var profile = self._lastProfile; - - return getMediaStreamAudioTracks(self._currentPlayOptions.mediaSource).filter(function (stream) { - return isAudioStreamSupported(stream, profile); - }); - } - - self.setAudioStreamIndex = function (index) { - - var streams = getSupportedAudioStreams(); - - if (streams.length < 2) { - // If there's only one supported stream then trust that the player will handle it on it's own - return; - } - - var audioIndex = -1; - var i; - var length; - var stream; - - for (i = 0, length = streams.length; i < length; i++) { - stream = streams[i]; - - audioIndex++; - - if (stream.Index === index) { - break; - } - } - - if (audioIndex === -1) { - return; - } - - var elem = self._mediaElement; - if (!elem) { - return; - } - - // https://msdn.microsoft.com/en-us/library/hh772507(v=vs.85).aspx - - var elemAudioTracks = elem.audioTracks || []; - console.debug('found ' + elemAudioTracks.length + ' audio tracks'); - - for (i = 0, length = elemAudioTracks.length; i < length; i++) { - - if (audioIndex === i) { - console.debug('setting audio track ' + i + ' to enabled'); - elemAudioTracks[i].enabled = true; - } else { - console.debug('setting audio track ' + i + ' to disabled'); - elemAudioTracks[i].enabled = false; - } - } - }; - - self.stop = function (destroyPlayer) { - var elem = self._mediaElement; - var src = self._currentSrc; - - if (elem) { - if (src) { - elem.pause(); - } - - htmlMediaHelper.onEndedInternal(self, elem, onError); - - if (destroyPlayer) { - self.destroy(); - } - } - - destroyCustomTrack(elem); - - return Promise.resolve(); - }; - - self.destroy = function () { - htmlMediaHelper.destroyHlsPlayer(self); - htmlMediaHelper.destroyFlvPlayer(self); - - appRouter.setTransparency('none'); - - var videoElement = self._mediaElement; - - if (videoElement) { - self._mediaElement = null; - - destroyCustomTrack(videoElement); - videoElement.removeEventListener('timeupdate', onTimeUpdate); - videoElement.removeEventListener('ended', onEnded); - videoElement.removeEventListener('volumechange', onVolumeChange); - videoElement.removeEventListener('pause', onPause); - videoElement.removeEventListener('playing', onPlaying); - videoElement.removeEventListener('play', onPlay); - videoElement.removeEventListener('click', onClick); - videoElement.removeEventListener('dblclick', onDblClick); - - videoElement.parentNode.removeChild(videoElement); - } - - var dlg = videoDialog; - if (dlg) { - videoDialog = null; - dlg.parentNode.removeChild(dlg); - } - - if (screenfull.isEnabled) { - screenfull.exit(); - } - }; - - function onEnded() { - - destroyCustomTrack(this); - htmlMediaHelper.onEndedInternal(self, this, onError); - } - - function onTimeUpdate(e) { - // get the player position and the transcoding offset - var time = this.currentTime; - - if (time && !self._timeUpdated) { - self._timeUpdated = true; - ensureValidVideo(this); - } - - self._currentTime = time; - - var currentPlayOptions = self._currentPlayOptions; - // Not sure yet how this is coming up null since we never null it out, but it is causing app crashes - if (currentPlayOptions) { - var timeMs = time * 1000; - timeMs += ((currentPlayOptions.transcodingOffsetTicks || 0) / 10000); - updateSubtitleText(timeMs); - } - - events.trigger(self, 'timeupdate'); - } - - function onVolumeChange() { - htmlMediaHelper.saveVolume(this.volume); - events.trigger(self, 'volumechange'); - } - - function onNavigatedToOsd() { - var dlg = videoDialog; - if (dlg) { - dlg.classList.remove('videoPlayerContainer-withBackdrop'); - dlg.classList.remove('videoPlayerContainer-onTop'); - - onStartedAndNavigatedToOsd(); - } - } - - function onStartedAndNavigatedToOsd() { - // If this causes a failure during navigation we end up in an awkward UI state - setCurrentTrackElement(subtitleTrackIndexToSetOnPlaying); - - if (audioTrackIndexToSetOnPlaying != null && self.canSetAudioStreamIndex()) { - self.setAudioStreamIndex(audioTrackIndexToSetOnPlaying); - } - } - - function onPlaying(e) { - if (!self._started) { - self._started = true; - this.removeAttribute('controls'); - - loading.hide(); - - htmlMediaHelper.seekOnPlaybackStart(self, e.target, self._currentPlayOptions.playerStartPositionTicks, function () { - if (currentSubtitlesOctopus) { - currentSubtitlesOctopus.timeOffset = (self._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + currentTrackOffset; - currentSubtitlesOctopus.resize(); - currentSubtitlesOctopus.resetRenderAheadCache(false); - } - }); - - if (self._currentPlayOptions.fullscreen) { - - appRouter.showVideoOsd().then(onNavigatedToOsd); - - } else { - appRouter.setTransparency('backdrop'); - videoDialog.classList.remove('videoPlayerContainer-withBackdrop'); - videoDialog.classList.remove('videoPlayerContainer-onTop'); - - onStartedAndNavigatedToOsd(); - } - } - events.trigger(self, 'playing'); - } - - function onPlay(e) { - events.trigger(self, 'unpause'); - } - - function ensureValidVideo(elem) { - if (elem !== self._mediaElement) { - return; - } - - if (elem.videoWidth === 0 && elem.videoHeight === 0) { - var mediaSource = (self._currentPlayOptions || {}).mediaSource; - - // 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) - if (!mediaSource || mediaSource.RunTimeTicks) { - htmlMediaHelper.onErrorInternal(self, 'mediadecodeerror'); - return; - } - } - } - - function onClick() { - events.trigger(self, 'click'); - } - - function onDblClick() { - events.trigger(self, 'dblclick'); - } - - function onPause() { - events.trigger(self, 'pause'); - } - - function onError() { - var errorCode = this.error ? (this.error.code || 0) : 0; - var errorMessage = this.error ? (this.error.message || '') : ''; - console.error('media element error: ' + errorCode.toString() + ' ' + errorMessage); - - var type; - - switch (errorCode) { - case 1: - // MEDIA_ERR_ABORTED - // This will trigger when changing media while something is playing - return; - case 2: - // MEDIA_ERR_NETWORK - type = 'network'; - break; - case 3: - // MEDIA_ERR_DECODE - if (self._hlsPlayer) { - htmlMediaHelper.handleHlsJsMediaError(self); - return; - } else { - type = 'mediadecodeerror'; - } - break; - case 4: - // MEDIA_ERR_SRC_NOT_SUPPORTED - type = 'medianotsupported'; - break; - default: - // seeing cases where Edge is firing error events with no error code - // example is start playing something, then immediately change src to something else - return; - } - - htmlMediaHelper.onErrorInternal(self, type); - } - - function destroyCustomTrack(videoElement) { - if (self._resizeObserver) { - self._resizeObserver.disconnect(); - self._resizeObserver = null; - } - - if (videoSubtitlesElem) { - var subtitlesContainer = videoSubtitlesElem.parentNode; - if (subtitlesContainer) { - tryRemoveElement(subtitlesContainer); - } - videoSubtitlesElem = null; - } - - currentTrackEvents = null; - - if (videoElement) { - var allTracks = videoElement.textTracks || []; // get list of tracks - for (var i = 0; i < allTracks.length; i++) { - - var currentTrack = allTracks[i]; - - if (currentTrack.label.indexOf('manualTrack') !== -1) { - currentTrack.mode = 'disabled'; - } - } - } - - customTrackIndex = -1; - currentClock = null; - self._currentAspectRatio = null; - - var octopus = currentSubtitlesOctopus; - if (octopus) { - octopus.dispose(); - } - currentSubtitlesOctopus = null; - - var renderer = currentAssRenderer; - if (renderer) { - renderer.setEnabled(false); - } - currentAssRenderer = null; - } - - self.destroyCustomTrack = destroyCustomTrack; - - function fetchSubtitlesUwp(track, item) { - return Windows.Storage.StorageFile.getFileFromPathAsync(track.Path).then(function (storageFile) { - return Windows.Storage.FileIO.readTextAsync(storageFile).then(function (text) { - return JSON.parse(text); - }); - }); - } - - function fetchSubtitles(track, item) { - if (window.Windows && itemHelper.isLocalItem(item)) { - return fetchSubtitlesUwp(track, item); - } - - incrementFetchQueue(); - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - - var url = getTextTrackUrl(track, item, '.js'); - - xhr.open('GET', url, true); - - xhr.onload = function (e) { - resolve(JSON.parse(this.response)); - decrementFetchQueue(); - }; - - xhr.onerror = function (e) { - reject(e); - decrementFetchQueue(); - }; - - xhr.send(); - }); - } - - function setTrackForDisplay(videoElement, track) { - - if (!track) { - destroyCustomTrack(videoElement); - return; - } - - // skip if already playing this track - if (customTrackIndex === track.Index) { - return; - } - - self.resetSubtitleOffset(); - var item = self._currentPlayOptions.item; - - destroyCustomTrack(videoElement); - customTrackIndex = track.Index; - renderTracksEvents(videoElement, track, item); - lastCustomTrackMs = 0; - } - - function renderSsaAss(videoElement, track, item) { - var attachments = self._currentPlayOptions.mediaSource.MediaAttachments || []; - var apiClient = connectionManager.getApiClient(item); - var options = { - video: videoElement, - subUrl: getTextTrackUrl(track, item), - fonts: attachments.map(function (i) { - return apiClient.getUrl(i.DeliveryUrl); - }), - workerUrl: appRouter.baseUrl() + '/libraries/subtitles-octopus-worker.js', - legacyWorkerUrl: appRouter.baseUrl() + '/libraries/subtitles-octopus-worker-legacy.js', - onError: function() { - htmlMediaHelper.onErrorInternal(self, 'mediadecodeerror'); - }, - timeOffset: (self._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000, - - // new octopus options; override all, even defaults - renderMode: 'blend', - dropAllAnimations: false, - libassMemoryLimit: 40, - libassGlyphLimit: 40, - targetFps: 24, - prescaleTradeoff: 0.8, - softHeightLimit: 1080, - hardHeightLimit: 2160, - resizeVariation: 0.2, - renderAhead: 90 - }; - require(['JavascriptSubtitlesOctopus'], function(SubtitlesOctopus) { - currentSubtitlesOctopus = new SubtitlesOctopus(options); - }); - } - - function requiresCustomSubtitlesElement() { - - // after a system update, ps4 isn't showing anything when creating a track element dynamically - // going to have to do it ourselves - if (browser.ps4) { - return true; - } - - // This is unfortunate, but we're unable to remove the textTrack that gets added via addTextTrack - if (browser.firefox || browser.web0s) { - return true; - } - - if (browser.edge) { - return true; - } - - if (browser.iOS) { - var userAgent = navigator.userAgent.toLowerCase(); - // works in the browser but not the native app - if ((userAgent.indexOf('os 9') !== -1 || userAgent.indexOf('os 8') !== -1) && userAgent.indexOf('safari') === -1) { - return true; - } - } - - return false; - } - - function renderSubtitlesWithCustomElement(videoElement, track, item) { - fetchSubtitles(track, item).then(function (data) { - if (!videoSubtitlesElem) { - var subtitlesContainer = document.createElement('div'); - subtitlesContainer.classList.add('videoSubtitles'); - subtitlesContainer.innerHTML = '
'; - videoSubtitlesElem = subtitlesContainer.querySelector('.videoSubtitlesInner'); - setSubtitleAppearance(subtitlesContainer, videoSubtitlesElem); - videoElement.parentNode.appendChild(subtitlesContainer); - currentTrackEvents = data.TrackEvents; - } - }); - } - - function setSubtitleAppearance(elem, innerElem) { - require(['userSettings', 'subtitleAppearanceHelper'], function (userSettings, subtitleAppearanceHelper) { - subtitleAppearanceHelper.applyStyles({ - text: innerElem, - window: elem - }, userSettings.getSubtitleAppearanceSettings()); - }); - } - - function getCueCss(appearance, selector) { - - var html = selector + '::cue {'; - - html += appearance.text.map(function (s) { - - return s.name + ':' + s.value + '!important;'; - - }).join(''); - - html += '}'; - - return html; - } - - function setCueAppearance() { - - require(['userSettings', 'subtitleAppearanceHelper'], function (userSettings, subtitleAppearanceHelper) { - - var elementId = self.id + '-cuestyle'; - - var styleElem = document.querySelector('#' + elementId); - if (!styleElem) { - styleElem = document.createElement('style'); - styleElem.id = elementId; - styleElem.type = 'text/css'; - document.getElementsByTagName('head')[0].appendChild(styleElem); - } - - styleElem.innerHTML = getCueCss(subtitleAppearanceHelper.getStyles(userSettings.getSubtitleAppearanceSettings(), true), '.htmlvideoplayer'); - }); - } - - function renderTracksEvents(videoElement, track, item) { - - if (!itemHelper.isLocalItem(item) || track.IsExternal) { - var format = (track.Codec || '').toLowerCase(); - if (format === 'ssa' || format === 'ass') { - renderSsaAss(videoElement, track, item); - return; - } - - if (requiresCustomSubtitlesElement()) { - renderSubtitlesWithCustomElement(videoElement, track, item); - return; - } - } - - var trackElement = null; - if (videoElement.textTracks && videoElement.textTracks.length > 0) { - trackElement = videoElement.textTracks[0]; - - // This throws an error in IE, but is fine in chrome - // In IE it's not necessary anyway because changing the src seems to be enough - try { - trackElement.mode = 'showing'; - while (trackElement.cues.length) { - trackElement.removeCue(trackElement.cues[0]); - } - } catch (e) { - console.error('error removing cue from textTrack'); - } - - trackElement.mode = 'disabled'; - } else { - // There is a function addTextTrack but no function for removeTextTrack - // Therefore we add ONE element and replace its cue data - trackElement = videoElement.addTextTrack('subtitles', 'manualTrack', 'und'); - } - - // download the track json - fetchSubtitles(track, item).then(function (data) { - - // show in ui - console.debug('downloaded ' + data.TrackEvents.length + ' track events'); - // add some cues to show the text - // in safari, the cues need to be added before setting the track mode to showing - data.TrackEvents.forEach(function (trackEvent) { - - var trackCueObject = window.VTTCue || window.TextTrackCue; - var cue = new trackCueObject(trackEvent.StartPositionTicks / 10000000, trackEvent.EndPositionTicks / 10000000, normalizeTrackEventText(trackEvent.Text, false)); - - trackElement.addCue(cue); - }); - trackElement.mode = 'showing'; - }); - } - - function updateSubtitleText(timeMs) { - - var clock = currentClock; - if (clock) { - try { - clock.seek(timeMs / 1000); - } catch (err) { - console.error('error in libjass: ' + err); - } - return; - } - - var trackEvents = currentTrackEvents; - var subtitleTextElement = videoSubtitlesElem; - - if (trackEvents && subtitleTextElement) { - var ticks = timeMs * 10000; - var selectedTrackEvent; - for (var i = 0; i < trackEvents.length; i++) { - - var currentTrackEvent = trackEvents[i]; - if (currentTrackEvent.StartPositionTicks <= ticks && currentTrackEvent.EndPositionTicks >= ticks) { - selectedTrackEvent = currentTrackEvent; - break; - } - } - - if (selectedTrackEvent && selectedTrackEvent.Text) { - subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true); - subtitleTextElement.classList.remove('hide'); - - } else { - subtitleTextElement.classList.add('hide'); - } - } - } - - function setCurrentTrackElement(streamIndex) { - - console.debug('setting new text track index to: ' + streamIndex); - - var mediaStreamTextTracks = getMediaStreamTextTracks(self._currentPlayOptions.mediaSource); - - var track = streamIndex === -1 ? null : mediaStreamTextTracks.filter(function (t) { - return t.Index === streamIndex; - })[0]; - - setTrackForDisplay(self._mediaElement, track); - if (enableNativeTrackSupport(self._currentSrc, track)) { - if (streamIndex !== -1) { - setCueAppearance(); - } - - } else { - // null these out to disable the player's native display (handled below) - streamIndex = -1; - track = null; - } - } - - function createMediaElement(options) { - - if (browser.tv || browser.iOS || browser.mobile) { - // too slow - // also on iOS, the backdrop image doesn't look right - // on android mobile, it works, but can be slow to have the video surface fully cover the backdrop - options.backdropUrl = null; - } - return new Promise(function (resolve, reject) { - - var dlg = document.querySelector('.videoPlayerContainer'); - - if (!dlg) { - - require(['css!./style'], function () { - - loading.show(); - - var dlg = document.createElement('div'); - - dlg.classList.add('videoPlayerContainer'); - - if (options.backdropUrl) { - dlg.classList.add('videoPlayerContainer-withBackdrop'); - dlg.style.backgroundImage = "url('" + options.backdropUrl + "')"; - } - - if (options.fullscreen) { - dlg.classList.add('videoPlayerContainer-onTop'); - } - - var html = ''; - var cssClass = 'htmlvideoplayer'; - - if (!browser.chromecast) { - cssClass += ' htmlvideoplayer-moveupsubtitles'; - } - - // Can't autoplay in these browsers so we need to use the full controls, at least until playback starts - if (!appHost.supports('htmlvideoautoplay')) { - html += '
${ApiKeysCaption}
${HeaderApiKey}${HeaderApp}${HeaderDateIssued}${HeaderApiKey}${HeaderApp}${HeaderDateIssued}