diff --git a/.ci/azure-pipelines-build.yml b/.ci/azure-pipelines-build.yml new file mode 100644 index 0000000000..fb08254216 --- /dev/null +++ b/.ci/azure-pipelines-build.yml @@ -0,0 +1,57 @@ +jobs: +- job: Build + displayName: 'Build' + + strategy: + matrix: + Development: + BuildConfiguration: development + Production: + BuildConfiguration: production + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: NodeTool@0 + displayName: 'Install Node' + inputs: + versionSpec: '12.x' + + - task: Cache@2 + displayName: 'Cache node_modules' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + + - script: 'yarn install --frozen-lockfile' + displayName: 'Install Dependencies' + env: + SKIP_PREPARE: '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: '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..8d9efbd73a --- /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: 'Cache node_modules' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + + - script: 'yarn install --frozen-lockfile' + displayName: 'Install Dependencies' + env: + SKIP_PREPARE: '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/.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/.eslintignore b/.eslintignore index 8e3aee83fb..74b18ddcf6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,3 @@ node_modules dist .idea .vscode -src/libraries diff --git a/.eslintrc.js b/.eslintrc.js index 4a3fec9448..aabfd633f8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,9 @@ +const restrictedGlobals = require('confusing-browser-globals'); + module.exports = { root: true, plugins: [ + '@babel', 'promise', 'import', 'eslint-comments' @@ -22,33 +25,40 @@ module.exports = { 'eslint:recommended', // 'plugin:promise/recommended', 'plugin:import/errors', - 'plugin:import/warnings', 'plugin:eslint-comments/recommended', '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-restricted-globals': ['error'].concat(restrictedGlobals), + 'no-trailing-spaces': ['error'], + '@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], + 'one-var': ['error', 'never'], + 'padded-blocks': ['error', 'never'], + 'prefer-const': ['error', {'destructuring': 'all'}], + 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], + '@babel/semi': ['error'], + 'no-var': ['error'], + 'space-before-blocks': ['error'], + 'space-infix-ops': 'error', + 'yoda': 'error' }, overrides: [ { files: [ './src/**/*.js' ], + parser: '@babel/eslint-parser', env: { node: false, amd: true, @@ -68,17 +78,12 @@ module.exports = { // Dependency globals '$': 'readonly', 'jQuery': 'readonly', - 'requirejs': 'readonly', // Jellyfin globals 'ApiClient': 'writable', - 'AppInfo': 'writable', 'chrome': 'writable', - 'ConnectionManager': 'writable', 'DlnaProfilePage': 'writable', - 'Dashboard': 'writable', 'DashboardPage': 'writable', 'Emby': 'readonly', - 'Events': 'writable', 'getParameterByName': 'writable', 'getWindowLocationSearch': 'writable', 'Globalize': 'writable', @@ -88,19 +93,17 @@ module.exports = { 'LinkParser': 'writable', 'LiveTvHelpers': 'writable', 'MetadataEditor': 'writable', - 'pageClassOn': 'writable', - 'pageIdOn': 'writable', 'PlaylistViewer': 'writable', 'UserParentalControlPage': 'writable', 'Windows': 'readonly' }, 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'], + 'no-unused-vars': ['off'], // TODO: Remove after ES6 migration is complete - 'import/no-unresolved': ["off"] + 'import/no-unresolved': ['off'] }, settings: { polyfills: [ @@ -130,6 +133,7 @@ module.exports = { 'Object.getOwnPropertyDescriptor', 'Object.getPrototypeOf', 'Object.keys', + 'Object.entries', 'Object.getOwnPropertyNames', 'Function.name', 'Function.hasInstance', @@ -190,4 +194,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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..f94934b447 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,31 @@ +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: '30 7 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + queries: +security-extended + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 9bccd32fb8..98aa2d974b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ -# config -config.json - # npm dist web node_modules +# config +config.json + # ide .idea .vscode -#log +# log yarn-error.log diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2eae7e6933..1bdf1cd903 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -34,7 +34,13 @@ - [Ryan Hartzell](https://github.com/ryan-hartzell) - [Thibault Nocchi](https://github.com/ThibaultNocchi) - [MrTimscampi](https://github.com/MrTimscampi) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [Sarab Singh](https://github.com/sarab97) + - [GuilhermeHideki](https://github.com/GuilhermeHideki) + - [Andrei Oanca](https://github.com/OancaAndrei) + - [Cromefire_](https://github.com/cromefire) + - [Orry Verducci](https://github.com/orryverducci) + - [Camc314](https://github.com/camc314) # 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/debian/conffiles b/debian/conffiles new file mode 100644 index 0000000000..a4b2c557e9 --- /dev/null +++ b/debian/conffiles @@ -0,0 +1 @@ +/usr/share/jellyfin/web/config.json 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..c35a1caab2 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,8 +12,11 @@ Source0: jellyfin-web-%{version}.tar.gz %if 0%{?centos} BuildRequires: yarn %else -BuildRequires nodejs-yarn +BuildRequires: nodejs-yarn %endif +# sadly the yarn RPM at https://dl.yarnpkg.com/rpm/ uses git but doesn't Requires: it +# ditto for Fedora's yarn RPM +BuildRequires: git BuildArch: noarch # Disable Automatic Dependency Processing @@ -39,5 +42,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 61b67829b2..c7da5adb11 100644 --- a/package.json +++ b/package.json @@ -5,27 +5,31 @@ "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/polyfill": "^7.8.7", - "@babel/preset-env": "^7.8.6", - "autoprefixer": "^9.7.6", - "babel-loader": "^8.0.6", - "browser-sync": "^2.26.7", + "@babel/core": "^7.12.3", + "@babel/eslint-parser": "^7.12.1", + "@babel/eslint-plugin": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.10.1", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "autoprefixer": "^9.8.6", + "babel-loader": "^8.2.1", + "browser-sync": "^2.26.13", "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^5.1.1", - "css-loader": "^3.4.2", + "confusing-browser-globals": "^1.0.10", + "copy-webpack-plugin": "^6.0.3", + "css-loader": "^5.0.1", "cssnano": "^4.1.10", - "del": "^5.1.0", - "eslint": "^6.8.0", + "del": "^6.0.0", + "eslint": "^7.13.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.22.1", "eslint-plugin-promise": "^4.2.1", - "file-loader": "^6.0.0", + "expose-loader": "^1.0.1", + "file-loader": "^6.2.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", @@ -34,88 +38,75 @@ "gulp-mode": "^1.0.2", "gulp-postcss": "^8.0.0", "gulp-sass": "^4.0.2", - "gulp-sourcemaps": "^2.6.5", - "gulp-terser": "^1.2.0", - "html-webpack-plugin": "^4.3.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-terser": "^1.4.1", + "html-loader": "^1.1.0", + "html-webpack-plugin": "^4.5.0", "lazypipe": "^1.0.2", - "node-sass": "^4.13.1", + "node-sass": "^5.0.0", "postcss-loader": "^3.0.0", "postcss-preset-env": "^6.7.0", - "style-loader": "^1.1.3", - "stylelint": "^13.3.3", + "source-map-loader": "^1.1.1", + "style-loader": "^2.0.0", + "stylelint": "^13.7.2", "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", + "stylelint-order": "^4.1.0", + "webpack": "^5.4.0", + "webpack-cli": "^4.0.0", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^6.1.1", + "workbox-webpack-plugin": "^5.1.4", + "worker-plugin": "^5.0.0" }, "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.13.0", - "document-register-element": "^1.14.3", - "fast-text-encoding": "^1.0.1", + "core-js": "^3.7.0", + "date-fns": "^2.16.1", + "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", + "headroom.js": "^0.12.0", + "hls.js": "^0.14.16", + "howler": "^2.2.1", + "intersection-observer": "^0.11.0", + "jellyfin-apiclient": "^1.4.2", "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.5.1", - "jstree": "^3.3.7", + "jstree": "^3.3.10", + "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", - "material-design-icons-iconfont": "^5.0.1", + "material-design-icons-iconfont": "^6.1.0", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", - "query-string": "^6.11.1", + "pdfjs-dist": "2.5.207", "resize-observer-polyfill": "^1.5.1", + "sass": "^1.29.0", + "sass-loader": "^10.0.5", "screenfull": "^5.0.2", - "shaka-player": "^2.5.11", - "sortablejs": "^1.10.2", - "swiper": "^5.3.7", + "sortablejs": "^1.12.0", + "swiper": "^6.3.5", "webcomponents.js": "^0.7.24", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.5.0", + "workbox-core": "^5.1.4", + "workbox-precaching": "^5.1.4" }, "babel": { "presets": [ - "@babel/preset-env" + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": 3 + } + ] ], - "overrides": [ - { - "test": [ - "src/components/autoFocuser.js", - "src/components/cardbuilder/cardBuilder.js", - "src/components/filedownloader.js", - "src/components/images/imageLoader.js", - "src/components/lazyloader/lazyloader-intersectionobserver.js", - "src/components/playback/mediasession.js", - "src/components/sanatizefilename.js", - "src/components/scrollManager.js", - "src/components/subtitleuploader/subtitleuploader.js", - "src/scripts/dfnshelper.js", - "src/scripts/dom.js", - "src/scripts/filesystem.js", - "src/scripts/imagehelper.js", - "src/scripts/inputManager.js", - "src/components/deletehelper.js", - "src/components/actionsheet/actionsheet.js", - "src/components/playmenu.js", - "src/components/indicators/indicators.js", - "src/scripts/keyboardnavigation.js", - "src/scripts/settings/appSettings.js", - "src/scripts/settings/userSettings.js", - "src/scripts/settings/webSettings.js" - ], - "plugins": [ - "@babel/plugin-transform-modules-amd" - ] - } + "plugins": [ + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-private-methods" ] }, "browserslist": [ @@ -123,7 +114,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", @@ -131,14 +122,15 @@ "Chrome 53", "Chrome 56", "Chrome 63", + "Edge 18", "Firefox ESR" ], "scripts": { - "serve": "gulp serve --development", - "prepare": "gulp --production", - "build:development": "gulp --development", - "build:production": "gulp --production", - "build:standalone": "gulp standalone --development", + "start": "yarn serve", + "serve": "webpack serve --config webpack.dev.js", + "prepare": "./scripts/prepare.sh", + "build:development": "webpack --config webpack.dev.js", + "build:production": "webpack --config webpack.prod.js", "lint": "eslint \".\"", "stylelint": "stylelint \"src/**/*.css\"" } diff --git a/scripts/duplicates.py b/scripts/duplicates.py new file mode 100644 index 0000000000..2daad94682 --- /dev/null +++ b/scripts/duplicates.py @@ -0,0 +1,33 @@ +import sys +import os +import json + +# load every string in the source language +# print all duplicate values to a file + +cwd = os.getcwd() +source = cwd + '/../src/strings/en-us.json' + +reverse = {} +duplicates = {} + +with open(source) as en: + strings = json.load(en) + for key, value in strings.items(): + if value not in reverse: + reverse[value] = [key] + else: + reverse[value].append(key) + +for key, value in reverse.items(): + if len(value) > 1: + duplicates[key] = value + +print('LENGTH: ' + str(len(duplicates))) +with open('duplicates.txt', 'w') as out: + for item in duplicates: + out.write(json.dumps(item) + ': ') + out.write(json.dumps(duplicates[item]) + '\n') + out.close() + +print('DONE') diff --git a/scripts/prepare.sh b/scripts/prepare.sh new file mode 100755 index 0000000000..bde12b36a5 --- /dev/null +++ b/scripts/prepare.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +if [ -z "${SKIP_PREPARE}" ]; then + webpack --config webpack.prod.js +fi diff --git a/scripts/scrm.py b/scripts/remove.py similarity index 95% rename from scripts/scrm.py rename to scripts/remove.py index 9bd5bc2a48..dba48537aa 100644 --- a/scripts/scrm.py +++ b/scripts/remove.py @@ -11,7 +11,7 @@ langlst = os.listdir(langdir) keys = [] -with open('scout.txt', 'r') as f: +with open('unused.txt', 'r') as f: for line in f: keys.append(line.strip('\n')) diff --git a/scripts/scdup.py b/scripts/source.py similarity index 80% rename from scripts/scdup.py rename to scripts/source.py index 468e31f14a..9b9ddf6466 100644 --- a/scripts/scdup.py +++ b/scripts/source.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/unused.py similarity index 84% rename from scripts/scgen.py rename to scripts/unused.py index 0d831426e6..abbc399cf6 100644 --- a/scripts/scgen.py +++ b/scripts/unused.py @@ -16,7 +16,7 @@ langlst.append('en-us.json') dep = [] def grep(key): - command = 'grep -r -E "(\(\\\"|\(\'|\{)%s(\\\"|\'|\})" --include=\*.{js,html} --exclude-dir=../src/strings ../src' % key + command = 'grep -r -E "(\\\"|\'|\{)%s(\\\"|\'|\})" --include=\*.{js,html} --exclude-dir=../src/strings ../src' % key p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output = p.stdout.readlines() if output: @@ -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/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/clearbutton.css b/src/assets/css/clearbutton.scss similarity index 100% rename from src/assets/css/clearbutton.css rename to src/assets/css/clearbutton.scss 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/detailtable.css b/src/assets/css/detailtable.scss similarity index 100% rename from src/assets/css/detailtable.css rename to src/assets/css/detailtable.scss diff --git a/src/assets/css/flexstyles.css b/src/assets/css/flexstyles.scss similarity index 82% rename from src/assets/css/flexstyles.css rename to src/assets/css/flexstyles.scss index a5a479f2f5..429ed7a650 100644 --- a/src/assets/css/flexstyles.css +++ b/src/assets/css/flexstyles.scss @@ -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/fonts.css b/src/assets/css/fonts.css deleted file mode 100644 index cb0da0f80f..0000000000 --- a/src/assets/css/fonts.css +++ /dev/null @@ -1,37 +0,0 @@ -html { - font-family: "Noto Sans", sans-serif; - font-size: 93%; - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} - -h1, -h2, -h3 { - font-family: "Noto Sans", sans-serif; -} - -h1 { - font-weight: 400; - font-size: 1.8em; -} - -h2 { - font-weight: 400; - font-size: 1.5em; -} - -h3 { - font-weight: 400; - font-size: 1.17em; -} - -.layout-tv { - font-size: 130%; -} - -.layout-mobile { - font-size: 90%; -} diff --git a/src/assets/css/fonts.scss b/src/assets/css/fonts.scss new file mode 100644 index 0000000000..32dc2e7bd6 --- /dev/null +++ b/src/assets/css/fonts.scss @@ -0,0 +1,34 @@ +@mixin font($weight: null, $size: null) { + font-family: "Noto Sans", sans-serif; + font-weight: $weight; + font-size: $size; +} + +html { + @include font; + text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +h1 { + @include font(400, 1.8em); +} + +h2 { + @include font(400, 1.5em); +} + +h3 { + @include font(400, 1.17em); +} + +.layout-tv { + /* Per WebOS and Tizen guidelines, fonts must be 20px minimum. + This takes the 16px baseline and multiplies it by 1.25 to get 20px. */ + font-size: 125%; +} + +.layout-mobile { + font-size: 90%; +} diff --git a/src/assets/css/fonts.sized.css b/src/assets/css/fonts.sized.css deleted file mode 100644 index f60a94f236..0000000000 --- a/src/assets/css/fonts.sized.css +++ /dev/null @@ -1,31 +0,0 @@ -h1 { - font-weight: 400; - font-size: 1.8em; -} - -.layout-desktop h1 { - font-size: 2em; -} - -h2 { - font-weight: 400; - font-size: 1.5em; -} - -h3 { - font-weight: 400; - font-size: 1.17em; -} - -@media all and (min-height: 720px) { - html { - font-size: 20px; - } -} - -/* This is supposed to be 1080p, but had to reduce the min height to account for possible browser chrome */ -@media all and (min-height: 1000px) { - html { - font-size: 27px; - } -} diff --git a/src/assets/css/fonts.sized.scss b/src/assets/css/fonts.sized.scss new file mode 100644 index 0000000000..1cec58a4a6 --- /dev/null +++ b/src/assets/css/fonts.sized.scss @@ -0,0 +1,31 @@ +@mixin header-font($size: null) { + font-weight: 400; + font-size: $size; +} + +html { + @media all and (min-height: 720px) { + font-size: 20px; + } + + /* This is supposed to be 1080p, but had to reduce the min height to account for possible browser chrome */ + @media all and (min-height: 1000px) { + font-size: 27px; + } +} + +h1 { + @include header-font(1.8em); + + .layout-desktop & { + font-size: 2em; + } +} + +h2 { + @include header-font(1.8em); +} + +h3 { + @include header-font(1.17em); +} diff --git a/src/assets/css/ios.css b/src/assets/css/ios.css deleted file mode 100644 index 57de0c5fdd..0000000000 --- a/src/assets/css/ios.css +++ /dev/null @@ -1,8 +0,0 @@ -html { - font-size: 82% !important; -} - -.formDialogFooter { - position: static !important; - margin: 0 -1em !important; -} diff --git a/src/assets/css/ios.scss b/src/assets/css/ios.scss new file mode 100644 index 0000000000..2b61f49a4e --- /dev/null +++ b/src/assets/css/ios.scss @@ -0,0 +1,3 @@ +html { + font-size: 82% !important; +} diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 82e704f074..c9ee82c8a0 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -24,14 +24,14 @@ padding-top: 7em !important; } -.layout-mobile .libraryPage { - padding-top: 4em !important; -} - .itemDetailPage { padding-top: 0 !important; } +.layout-tv .itemDetailPage { + padding-top: 4.2em !important; +} + .standalonePage { padding-top: 4.5em !important; } @@ -164,6 +164,13 @@ display: flex; flex-direction: column; contain: layout style paint; + transition: background ease-in-out 0.5s; +} + +.layout-tv .skinHeader { + /* In TV layout, it makes more sense to keep the top bar at the top of the page + Having it follow the view only makes us lose vertical space, while not being focusable */ + position: relative; } .hiddenViewMenuBar .skinHeader { @@ -178,6 +185,10 @@ width: 100%; } +.layout-tv .sectionTabs { + width: 55%; +} + .selectedMediaFolder { background-color: #f2f2f2 !important; } @@ -235,12 +246,6 @@ text-align: center; } -.layout-desktop .searchTabButton, -.layout-mobile .searchTabButton, -.layout-tv .headerSearchButton { - display: none !important; -} - .mainDrawer-scrollContainer { padding-bottom: 10vh; } @@ -272,7 +277,7 @@ } } -@media all and (max-width: 84em) { +@media all and (max-width: 100em) { .withSectionTabs .headerTop { padding-bottom: 0.55em; } @@ -280,9 +285,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,16 +447,17 @@ 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, -.layout-tv .itemBackdrop::after { +.layout-desktop .itemBackdrop::after { content: ""; width: 100%; height: 100%; @@ -455,18 +465,28 @@ display: block; } -.layout-desktop .noBackdrop .itemBackdrop, -.layout-tv .noBackdrop .itemBackdrop { +.layout-tv .itemBackdrop, +.layout-desktop .noBackdrop .itemBackdrop { display: none; } .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 +497,10 @@ margin-top: 0; } +.detailSectionContent a { + color: inherit; +} + .personBackdrop { background-size: contain; } @@ -495,7 +519,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 +543,6 @@ -webkit-box-align: center; -webkit-align-items: center; align-items: center; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; margin: 1em 0; } @@ -520,6 +558,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 +613,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; @@ -553,10 +633,14 @@ z-index: 2; } +.layout-tv .detailPagePrimaryContainer { + display: block; +} + .layout-mobile .detailPagePrimaryContainer { display: block; position: relative; - top: 0; + padding: 0.5em 3.3% 0.5em; } .layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, @@ -566,13 +650,18 @@ padding-left: 32.45vw; } -.layout-desktop .detailSticky, -.layout-tv .detailSticky { +.layout-desktop .detailRibbon { margin-top: -7.2em; + height: 7.2em; } -.layout-desktop .noBackdrop .detailSticky, -.layout-tv .noBackdrop .detailSticky { +.layout-tv .detailRibbon { + margin-top: 0; + height: inherit; +} + +.layout-desktop .noBackdrop .detailRibbon, +.layout-tv .noBackdrop .detailRibbon { margin-top: 0; } @@ -584,6 +673,9 @@ white-space: nowrap; text-overflow: ellipsis; text-align: left; + min-width: 0; + max-width: 100%; + overflow: hidden; } .layout-mobile .infoText { @@ -594,12 +686,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 +721,11 @@ } .detailLogo { - width: 30vw; - height: 25vh; + width: 25vw; + height: 16vh; position: absolute; top: 10vh; - right: 20vw; + right: 25vw; background-size: contain; } @@ -641,7 +750,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 +765,18 @@ div.itemDetailGalleryLink.defaultCardBackground { position: relative; } - .layout-desktop .detailPageWrapperContainer, - .layout-tv .detailPageWrapperContainer { - margin-top: 7.2em; + .layout-desktop .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 { @@ -677,13 +791,8 @@ div.itemDetailGalleryLink.defaultCardBackground { } .emby-button.detailFloatingButton { - position: absolute; - background-color: rgba(0, 0, 0, 0.5) !important; - z-index: 1; - top: 50%; - left: 50%; - margin: -2.2em 0 0 -2.2em; - padding: 0.4em !important; + font-size: 1.4em; + margin-right: 0.5em !important; color: rgba(255, 255, 255, 0.76); } @@ -693,16 +802,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) { @@ -750,7 +855,7 @@ div.itemDetailGalleryLink.defaultCardBackground { -webkit-align-items: center; align-items: center; margin: 0 !important; - padding: 0.5em 0.7em !important; + padding: 0.7em 0.7em !important; } @media all and (min-width: 29em) { @@ -797,9 +902,9 @@ div.itemDetailGalleryLink.defaultCardBackground { } .detailImageProgressContainer { - position: absolute; bottom: 0; - width: 22.786458333333332vw; + margin-top: -0.4vw; + width: 100%; } .detailButton-text { @@ -818,25 +923,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; - } - - .detailFloatingButton { - display: none !important; - } - +@media all and (min-width: 100em) { .personBackdrop { display: none !important; } @@ -845,6 +932,11 @@ div.itemDetailGalleryLink.defaultCardBackground { font-size: 108%; margin: 1.25em 0; } + + .layout-tv .mainDetailButtons { + font-size: 108%; + margin: 1em 0 1.25em; + } } @media all and (max-width: 50em) { @@ -866,6 +958,10 @@ div.itemDetailGalleryLink.defaultCardBackground { } } +.detailVerticalSection .emby-scrollbuttons { + padding-top: 0.4em; +} + .layout-tv .detailVerticalSection { margin-bottom: 3.4em !important; } @@ -954,6 +1050,10 @@ div.itemDetailGalleryLink.defaultCardBackground { margin-bottom: 2.7em; } +.layout-mobile .verticalSection-extrabottompadding { + margin-bottom: 1em; +} + .sectionTitleButton, .sectionTitleIconButton { margin-right: 0 !important; @@ -979,7 +1079,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 { @@ -1046,13 +1152,13 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { } .layout-tv .padded-top-focusscale { - padding-top: 1em; - margin-top: -1em; + padding-top: 1.5em; + margin-top: -1.5em; } .layout-tv .padded-bottom-focusscale { - padding-bottom: 1em; - margin-bottom: -1em; + padding-bottom: 1.5em; + margin-bottom: -1.5em; } @media all and (min-height: 31.25em) { @@ -1132,6 +1238,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 +1256,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/livetv.css b/src/assets/css/livetv.scss similarity index 63% rename from src/assets/css/livetv.css rename to src/assets/css/livetv.scss index 695adff8c5..032bcddf48 100644 --- a/src/assets/css/livetv.css +++ b/src/assets/css/livetv.scss @@ -2,8 +2,8 @@ padding-bottom: 15em; } -@media all and (min-width: 62.5em) { - #guideTab { +#guideTab { + @media all and (min-width: 62.5em) { padding-left: 0.5em; } } 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.scss similarity index 66% rename from src/assets/css/site.css rename to src/assets/css/site.scss index 627145abc1..3d4ea7da0e 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.scss @@ -1,10 +1,21 @@ -body, -html { +@mixin fullpage { margin: 0; padding: 0; height: 100%; } +html { + @include fullpage; + line-height: 1.35; +} + +body { + @include fullpage; + overflow-x: hidden; + background-color: transparent !important; + -webkit-font-smoothing: antialiased; +} + .clipForScreenReader { clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); @@ -18,7 +29,7 @@ html { .material-icons { /* Fix font ligatures on older WebOS versions */ - -webkit-font-feature-settings: "liga"; + font-feature-settings: "liga"; } .backgroundContainer { @@ -30,26 +41,12 @@ html { contain: strict; } -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; - -webkit-font-smoothing: antialiased; -} - .mainAnimatedPage { contain: style size !important; } @@ -62,7 +59,7 @@ body { overflow-y: hidden !important; } -div[data-role=page] { +div[data-role="page"] { outline: 0; } @@ -75,10 +72,10 @@ div[data-role=page] { padding-left: 0.15em; font-weight: 400; white-space: normal !important; -} -.fieldDescription + .fieldDescription { - margin-top: 0.3em; + + .fieldDescription { + margin-top: 0.3em; + } } .content-primary, @@ -89,9 +86,14 @@ div[data-role=page] { padding-bottom: 5em !important; } -@media all and (min-width: 50em) { - .readOnlyContent, - form { +.readOnlyContent { + @media all and (min-width: 50em) { + max-width: 54em; + } +} + +form { + @media all and (min-width: 50em) { max-width: 54em; } } @@ -111,12 +113,39 @@ div[data-role=page] { .headroom { will-change: transform; transition: transform 200ms linear; + + &--pinned { + transform: translateY(0%); + } + + &--unpinned { + transform: translateY(-100%); + } } -.headroom--pinned { - transform: translateY(0%); +.drawerContent { + /* make sure the bottom of the drawer is visible when music is playing */ + padding-bottom: 4em; } -.headroom--unpinned { - transform: translateY(-100%); +.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..b2446d5d48 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -6,31 +6,43 @@ -ms-user-select: none; } -.osdPoster img, -.pageContainer, .videoOsdBottom { bottom: 0; left: 0; right: 0; + position: fixed; + background: linear-gradient(0deg, rgba(16, 16, 16, 0.75) 0%, rgba(16, 16, 16, 0) 100%); + padding-top: 7.5em; + padding-bottom: 1.75em; + display: flex; + flex-direction: row; + justify-content: center; + will-change: opacity; + transition: opacity 0.3s ease-out; + color: #fff; + user-select: none; + -webkit-touch-callout: none; } -.osdHeader { - -webkit-transition: opacity 0.3s ease-out; - -o-transition: opacity 0.3s ease-out; +.skinHeader-withBackground.osdHeader { transition: opacity 0.3s ease-out; position: relative; z-index: 1; - background: rgba(0, 0, 0, 0.7) !important; - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; - color: #eee !important; + background: linear-gradient(180deg, rgba(16, 16, 16, 0.75) 0%, rgba(16, 16, 16, 0) 100%); + backdrop-filter: none; + color: #eee; + height: 7.5em; } .osdHeader-hidden { opacity: 0; } -.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton) { +.osdHeader .headerTop { + max-height: 3.5em; +} + +.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton):not(.headerSyncButton) { display: none; } @@ -87,34 +99,17 @@ opacity: 0.6; } -.videoOsdBottom { - position: fixed; - background-color: rgba(0, 0, 0, 0.7); - padding: 1%; - display: -webkit-box; - display: -webkit-flex; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -webkit-flex-direction: row; - flex-direction: row; - will-change: opacity; - -webkit-transition: opacity 0.3s ease-out; - -o-transition: opacity 0.3s ease-out; - transition: opacity 0.3s ease-out; - color: #fff; - user-select: none; - -webkit-touch-callout: none; -} - .videoOsdBottom-hidden { opacity: 0; } .osdControls { - -webkit-box-flex: 1; - -webkit-flex-grow: 1; flex-grow: 1; + padding: 0 0.8em; +} + +.layout-desktop .osdControls { + max-width: calc(100vh * 1.77 - 2vh); } .videoOsdBottom .buttons { @@ -146,7 +141,7 @@ } .volumeButtons { - margin: 0 0.5em 0 auto; + margin: 0 1em 0 0.29em; display: flex; -webkit-align-items: center; align-items: center; @@ -154,33 +149,13 @@ .osdTimeText { margin-left: 1em; + margin-right: auto; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } -.osdPoster { - width: 10%; - position: relative; - margin-right: 0.5em; -} - -.osdPoster img { - position: absolute; - height: auto; - width: 100%; - -webkit-box-shadow: 0 0 1.9vh #000; - box-shadow: 0 0 1.9vh #000; - border: 0.08em solid #222; - user-drag: none; - user-select: none; - -moz-user-select: none; - -webkit-user-drag: none; - -webkit-user-select: none; - -ms-user-select: none; -} - .osdTitle, .osdTitleSmall { margin: 0 1em 0 0; @@ -248,14 +223,7 @@ animation: spin 4s linear infinite; } -.pageContainer { - top: 0; - position: fixed; -} - @media all and (max-width: 30em) { - .btnFastForward, - .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; diff --git a/src/assets/img/avatar.png b/src/assets/img/avatar.png index 0453f24897..4023d1c5c7 100644 Binary files a/src/assets/img/avatar.png and b/src/assets/img/avatar.png differ 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/assets/img/icon-transparent.png b/src/assets/img/icon-transparent.png index 0d71b56067..76ef75baf9 100644 Binary files a/src/assets/img/icon-transparent.png and b/src/assets/img/icon-transparent.png differ diff --git a/src/assets/splash/ipad_splash.png b/src/assets/splash/ipad_splash.png index 0faca927b7..45ed7d398d 100755 Binary files a/src/assets/splash/ipad_splash.png and b/src/assets/splash/ipad_splash.png differ diff --git a/src/assets/splash/ipad_splash_l.png b/src/assets/splash/ipad_splash_l.png index 3ecf2d5adc..baf016e865 100644 Binary files a/src/assets/splash/ipad_splash_l.png and b/src/assets/splash/ipad_splash_l.png differ diff --git a/src/assets/splash/ipadpro1_splash.png b/src/assets/splash/ipadpro1_splash.png index 9e7fdb7f6c..c5afa50986 100755 Binary files a/src/assets/splash/ipadpro1_splash.png and b/src/assets/splash/ipadpro1_splash.png differ diff --git a/src/assets/splash/ipadpro1_splash_l.png b/src/assets/splash/ipadpro1_splash_l.png index cffad337c8..f5ef6f3b04 100644 Binary files a/src/assets/splash/ipadpro1_splash_l.png and b/src/assets/splash/ipadpro1_splash_l.png differ diff --git a/src/assets/splash/ipadpro2_splash.png b/src/assets/splash/ipadpro2_splash.png index a2e9624c65..500cad2b3d 100755 Binary files a/src/assets/splash/ipadpro2_splash.png and b/src/assets/splash/ipadpro2_splash.png differ diff --git a/src/assets/splash/ipadpro2_splash_l.png b/src/assets/splash/ipadpro2_splash_l.png index 588902a2b1..ec17e3e81b 100644 Binary files a/src/assets/splash/ipadpro2_splash_l.png and b/src/assets/splash/ipadpro2_splash_l.png differ diff --git a/src/assets/splash/ipadpro3_splash.png b/src/assets/splash/ipadpro3_splash.png index 89a04afe07..3a1c82e983 100755 Binary files a/src/assets/splash/ipadpro3_splash.png and b/src/assets/splash/ipadpro3_splash.png differ diff --git a/src/assets/splash/ipadpro3_splash_l.png b/src/assets/splash/ipadpro3_splash_l.png index a0b6c56904..df275b3448 100644 Binary files a/src/assets/splash/ipadpro3_splash_l.png and b/src/assets/splash/ipadpro3_splash_l.png differ diff --git a/src/assets/splash/iphone5_splash.png b/src/assets/splash/iphone5_splash.png index 3e073a9e1b..bdbc28ab0d 100755 Binary files a/src/assets/splash/iphone5_splash.png and b/src/assets/splash/iphone5_splash.png differ diff --git a/src/assets/splash/iphone5_splash_l.png b/src/assets/splash/iphone5_splash_l.png index 28fa8838e1..789f50f628 100644 Binary files a/src/assets/splash/iphone5_splash_l.png and b/src/assets/splash/iphone5_splash_l.png differ diff --git a/src/assets/splash/iphone6_splash.png b/src/assets/splash/iphone6_splash.png index afe42fa26a..879d5b716f 100755 Binary files a/src/assets/splash/iphone6_splash.png and b/src/assets/splash/iphone6_splash.png differ diff --git a/src/assets/splash/iphone6_splash_l.png b/src/assets/splash/iphone6_splash_l.png index c7de58510f..396224fcb2 100644 Binary files a/src/assets/splash/iphone6_splash_l.png and b/src/assets/splash/iphone6_splash_l.png differ diff --git a/src/assets/splash/iphoneplus_splash.png b/src/assets/splash/iphoneplus_splash.png index 2cc62163ac..a9e7de01ca 100755 Binary files a/src/assets/splash/iphoneplus_splash.png and b/src/assets/splash/iphoneplus_splash.png differ diff --git a/src/assets/splash/iphoneplus_splash_l.png b/src/assets/splash/iphoneplus_splash_l.png index c989f2237e..f14f0af1ff 100644 Binary files a/src/assets/splash/iphoneplus_splash_l.png and b/src/assets/splash/iphoneplus_splash_l.png differ diff --git a/src/assets/splash/iphonex_splash.png b/src/assets/splash/iphonex_splash.png index 2d9698c287..435bbe07a9 100755 Binary files a/src/assets/splash/iphonex_splash.png and b/src/assets/splash/iphonex_splash.png differ diff --git a/src/assets/splash/iphonex_splash_l.png b/src/assets/splash/iphonex_splash_l.png index 9eb9e037ce..cbadd0a239 100644 Binary files a/src/assets/splash/iphonex_splash_l.png and b/src/assets/splash/iphonex_splash_l.png differ diff --git a/src/assets/splash/iphonexr_splash.png b/src/assets/splash/iphonexr_splash.png index 0f911eb2e8..b6072df329 100755 Binary files a/src/assets/splash/iphonexr_splash.png and b/src/assets/splash/iphonexr_splash.png differ diff --git a/src/assets/splash/iphonexr_splash_l.png b/src/assets/splash/iphonexr_splash_l.png index 1522f372a9..f78e825192 100644 Binary files a/src/assets/splash/iphonexr_splash_l.png and b/src/assets/splash/iphonexr_splash_l.png differ diff --git a/src/assets/splash/iphonexsmax_splash.png b/src/assets/splash/iphonexsmax_splash.png index 3e9aaf56af..b4e9255b31 100755 Binary files a/src/assets/splash/iphonexsmax_splash.png and b/src/assets/splash/iphonexsmax_splash.png differ diff --git a/src/assets/splash/iphonexsmax_splash_l.png b/src/assets/splash/iphonexsmax_splash_l.png index e2961f512e..e94692de0e 100644 Binary files a/src/assets/splash/iphonexsmax_splash_l.png and b/src/assets/splash/iphonexsmax_splash_l.png differ diff --git a/src/bundle.js b/src/bundle.js deleted file mode 100644 index d7ba6c6a51..0000000000 --- a/src/bundle.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * require.js module definitions bundled by webpack - */ -// 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; -}); - -// query-string -var query = require('query-string'); -_define('queryString', function() { - return query; -}); - -// flvjs -var flvjs = require('flv.js/dist/flv').default; -_define('flvjs', function() { - return flvjs; -}); - -// jstree -var jstree = require('jstree'); -require('jstree/dist/themes/default/style.css'); -_define('jstree', function() { - return jstree; -}); - -// jquery -var jquery = require('jquery'); -_define('jQuery', function() { - return jquery; -}); - -// hlsjs -var hlsjs = require('hls.js'); -_define('hlsjs', function() { - return hlsjs; -}); - -// howler -var howler = require('howler'); -_define('howler', function() { - return howler; -}); - -// resize-observer-polyfill -var resize = require('resize-observer-polyfill').default; -_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'); -_define('swiper', function() { - return swiper; -}); - -// sortable -var sortable = require('sortablejs').default; -_define('sortable', function() { - return sortable; -}); - -// webcomponents -var webcomponents = require('webcomponents.js/webcomponents-lite'); -_define('webcomponents', function() { - return webcomponents; -}); - -// libass-wasm -var libassWasm = require('libass-wasm'); -_define('JavascriptSubtitlesOctopus', function() { - return libassWasm; -}); - -// material-icons -var materialIcons = require('material-design-icons-iconfont/dist/material-design-icons.css'); -_define('material-icons', function() { - return materialIcons; -}); - -// noto font -var noto = require('jellyfin-noto'); -_define('jellyfin-noto', function () { - return noto; -}); - -// page.js -var page = require('page'); -_define('page', function() { - return page; -}); - -// core-js -var polyfill = require('@babel/polyfill/dist/polyfill'); -_define('polyfill', function () { - return polyfill; -}); - -// domtokenlist-shim -var classlist = require('classlist.js'); -_define('classlist-polyfill', function () { - return classlist; -}); - -// Date-FNS -var dateFns = require('date-fns'); -_define('date-fns', function () { - return dateFns; -}); - -var dateFnsLocale = require('date-fns/locale'); -_define('date-fns/locale', function () { - return dateFnsLocale; -}); - -var fast_text_encoding = require('fast-text-encoding'); -_define('fast-text-encoding', function () { - return fast_text_encoding; -}); - -// intersection-observer -var intersection_observer = require('intersection-observer'); -_define('intersection-observer', function () { - return intersection_observer; -}); - -// screenfull -var screenfull = require('screenfull'); -_define('screenfull', function () { - return screenfull; -}); - -// headroom.js -var headroom = require('headroom.js/dist/headroom'); -_define('headroom', function () { - return headroom; -}); - -// apiclient -var apiclient = require('jellyfin-apiclient'); - -_define('apiclient', function () { - return apiclient.ApiClient; -}); - -_define('events', function () { - return apiclient.Events; -}); - -_define('credentialprovider', function () { - return apiclient.Credentials; -}); - -_define('connectionManagerFactory', function () { - return apiclient.ConnectionManager; -}); - -_define('appStorage', function () { - return apiclient.AppStorage; -}); diff --git a/src/components/AppInfo.js b/src/components/AppInfo.js new file mode 100644 index 0000000000..a89c55d0b1 --- /dev/null +++ b/src/components/AppInfo.js @@ -0,0 +1,4 @@ + +export default { + isNativeApp: false +}; diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js new file mode 100644 index 0000000000..316f42558d --- /dev/null +++ b/src/components/ServerConnections.js @@ -0,0 +1,82 @@ +import { ConnectionManager, Credentials, ApiClient, Events } from 'jellyfin-apiclient'; +import { appHost } from './apphost'; +import Dashboard from '../scripts/clientUtils'; +import AppInfo from './AppInfo'; +import { setUserInfo } from '../scripts/settings/userSettings'; + +class ServerConnections extends ConnectionManager { + constructor() { + super(...arguments); + this.localApiClient = null; + + Events.on(this, 'localusersignedout', function () { + setUserInfo(null, null); + }); + } + + initApiClient() { + if (!AppInfo.isNativeApp) { + console.debug('creating ApiClient singleton'); + + const apiClient = new ApiClient( + Dashboard.serverAddress(), + appHost.appName(), + appHost.appVersion(), + appHost.deviceName(), + appHost.deviceId() + ); + + apiClient.enableAutomaticNetworking = false; + apiClient.manualAddressOnly = true; + + this.addApiClient(apiClient); + + this.setLocalApiClient(apiClient); + + console.debug('loaded ApiClient singleton'); + } + } + + setLocalApiClient(apiClient) { + if (apiClient) { + this.localApiClient = apiClient; + window.ApiClient = apiClient; + } + } + + getLocalApiClient() { + return this.localApiClient; + } + + currentApiClient() { + let apiClient = this.getLocalApiClient(); + + if (!apiClient) { + const server = this.getLastUsedServer(); + + if (server) { + apiClient = this.getApiClient(server.Id); + } + } + + return apiClient; + } + + onLocalUserSignedIn(user) { + const apiClient = this.getApiClient(user.ServerId); + this.setLocalApiClient(apiClient); + return setUserInfo(user.Id, apiClient); + } +} + +const credentials = new Credentials(); + +const capabilities = Dashboard.capabilities(appHost); + +export default new ServerConnections( + credentials, + appHost.appName(), + appHost.appVersion(), + appHost.deviceName(), + appHost.deviceId(), + capabilities); diff --git a/src/components/accessSchedule/accessSchedule.js b/src/components/accessSchedule/accessSchedule.js new file mode 100644 index 0000000000..9e0e3d5cf9 --- /dev/null +++ b/src/components/accessSchedule/accessSchedule.js @@ -0,0 +1,98 @@ + +/* eslint-disable indent */ + +/** + * Module for controlling user parental control from. + * @module components/accessSchedule/accessSchedule + */ + +import dialogHelper from '../dialogHelper/dialogHelper'; +import datetime from '../../scripts/datetime'; +import globalize from '../../scripts/globalize'; +import '../../elements/emby-select/emby-select'; +import '../../elements/emby-button/paper-icon-button-light'; +import '../formdialog.css'; + + 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('./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 71% rename from src/components/accessschedule/accessschedule.template.html rename to src/components/accessSchedule/accessSchedule.template.html index 493150ae5e..c0f83ccec6 100644 --- a/src/components/accessschedule/accessschedule.template.html +++ b/src/components/accessSchedule/accessSchedule.template.html @@ -1,5 +1,5 @@
-

@@ -12,13 +12,13 @@
'; + 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 +171,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 +191,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 +199,70 @@ define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectio } function centerFocus(elem, horiz, on) { - require(['scrollHelper'], function (scrollHelper) { - var fn = on ? 'on' : 'off'; + import('../../scripts/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..1978324e7c 100644 --- a/src/components/confirm/confirm.js +++ b/src/components/confirm/confirm.js @@ -1,65 +1,65 @@ -define(['browser', 'dialog', 'globalize'], function(browser, dialog, globalize) { - 'use strict'; +import browser from '../../scripts/browser'; +import dialog from '../dialog/dialog'; +import globalize from '../../scripts/globalize'; - function replaceAll(str, find, replace) { - return str.split(find).join(replace); +function replaceAll(str, find, replace) { + return str.split(find).join(replace); +} + +function nativeConfirm(options) { + if (typeof options === 'string') { + options = { + title: '', + text: options + }; } - if (browser.tv && window.confirm) { - // Use the native confirm dialog - return function (options) { - if (typeof options === 'string') { - options = { - title: '', - text: options - }; - } + const text = replaceAll(options.text || '', '
', '\n'); + const result = window.confirm(text); - var text = replaceAll(options.text || '', '
', '\n'); - var result = confirm(text); + if (result) { + return Promise.resolve(); + } else { + return Promise.reject(); + } +} - if (result) { - return Promise.resolve(); - } else { - return Promise.reject(); - } +function customConfirm(text, title) { + let options; + if (typeof text === 'string') { + options = { + title: title, + text: text }; } else { - // Use our own dialog - return function (text, title) { - var options; - if (typeof text === 'string') { - options = { - title: title, - text: text - }; - } else { - options = text; - } - - var items = []; - - items.push({ - name: options.cancelText || globalize.translate('ButtonCancel'), - id: 'cancel', - type: 'cancel' - }); - - items.push({ - name: options.confirmText || globalize.translate('ButtonOk'), - id: 'ok', - type: options.primary === 'delete' ? 'delete' : 'submit' - }); - - options.buttons = items; - - return dialog(options).then(function (result) { - if (result === 'ok') { - return Promise.resolve(); - } - - return Promise.reject(); - }); - }; + options = text; } -}); + + const items = []; + + items.push({ + name: options.cancelText || globalize.translate('ButtonCancel'), + id: 'cancel', + type: 'cancel' + }); + + items.push({ + name: options.confirmText || globalize.translate('ButtonOk'), + id: 'ok', + type: options.primary === 'delete' ? 'delete' : 'submit' + }); + + options.buttons = items; + + return dialog.show(options).then(result => { + if (result === 'ok') { + return Promise.resolve(); + } + + return Promise.reject(); + }); +} + +const confirm = browser.tv && window.confirm ? nativeConfirm : customConfirm; + +export default confirm; diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index cfb5821b38..ee97fff8a1 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/dialogHelper'; +import dom from '../../scripts/dom'; +import layoutManager from '../layoutManager'; +import scrollHelper from '../../scripts/scrollHelper'; +import globalize from '../../scripts/globalize'; +import 'material-design-icons-iconfont'; +import '../../elements/emby-button/emby-button'; +import '../../elements/emby-button/paper-icon-button-light'; +import '../../elements/emby-input/emby-input'; +import '../formdialog.css'; +import '../../assets/css/flexstyles.scss'; + +/* 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('./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..cdaf47996f 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 '../../scripts/browser'; +import layoutManager from '../layoutManager'; +import inputManager from '../../scripts/inputManager'; +import dom from '../../scripts/dom'; +import './dialoghelper.css'; +import '../../assets/css/scrollstyles.css'; - 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,9 +85,9 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', } if (!self.closedByBack && isHistoryEnabled(dlg)) { - var state = history.state || {}; + const state = window.history.state || {}; if (state.dialogId === hash) { - history.back(); + window.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,40 +184,36 @@ 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(); + window.history.back(); } else { closeDialog(dlg); } @@ -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,14 +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; - } - if (options.lockScroll != null) { return options.lockScroll; } @@ -334,6 +318,10 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', return true; } + if (supportsOverscrollBehavior && (options.size || !browser.touch)) { + return false; + } + if (options.size) { return true; } @@ -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('../../scripts/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'); @@ -390,7 +375,7 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', dlg.setAttribute('data-lockscroll', 'true'); } - if (options.enableHistory !== false && appRouter.enableNativeHistory()) { + if (options.enableHistory === true) { dlg.setAttribute('data-history', 'true'); } @@ -406,17 +391,14 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', dlg.setAttribute('data-autofocus', 'true'); } - var defaultEntryAnimation; - var defaultExitAnimation; - - defaultEntryAnimation = 'scaleup'; - defaultExitAnimation = 'scaledown'; - var entryAnimation = options.entryAnimation || defaultEntryAnimation; - var exitAnimation = options.exitAnimation || defaultExitAnimation; + const defaultEntryAnimation = 'scaleup'; + const defaultExitAnimation = 'scaledown'; + 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 +443,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 +468,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..2f8a2cadd1 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -1,9 +1,20 @@ -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/loading'; +import dialogHelper from '../dialogHelper/dialogHelper'; +import dom from '../../scripts/dom'; +import globalize from '../../scripts/globalize'; +import '../listview/listview.css'; +import '../../elements/emby-input/emby-input'; +import '../../elements/emby-button/paper-icon-button-light'; +import './directorybrowser.css'; +import '../formdialog.css'; +import '../../elements/emby-button/emby-button'; +import alert from '../alert'; + +/* eslint-disable indent */ function getSystemInfo() { return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then( - function(info) { + info => { systemInfo = info; return info; } @@ -21,9 +32,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 +46,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 +57,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 +69,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 +80,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 +93,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 +116,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 +134,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,20 +158,19 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in } function alertTextWithOptions(options) { - require(['alert'], function(alert) { - alert(options); - }); + alert(options); } function validatePath(path, validateWriteable, apiClient) { return apiClient.ajax({ type: 'POST', url: apiClient.getUrl('Environment/ValidatePath'), - data: { + data: JSON.stringify({ ValidateWriteable: validateWriteable, Path: path - } - }).catch(function(response) { + }), + contentType: 'application/json' + }).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..9d7292547d --- /dev/null +++ b/src/components/displaySettings/displaySettings.js @@ -0,0 +1,247 @@ +import browser from '../../scripts/browser'; +import layoutManager from '../layoutManager'; +import { pluginManager } from '../pluginManager'; +import { appHost } from '../apphost'; +import focusManager from '../focusManager'; +import datetime from '../../scripts/datetime'; +import globalize from '../../scripts/globalize'; +import loading from '../loading/loading'; +import skinManager from '../../scripts/themeManager'; +import { Events } from 'jellyfin-apiclient'; +import '../../elements/emby-select/emby-select'; +import '../../elements/emby-checkbox/emby-checkbox'; +import '../../elements/emby-button/emby-button'; +import ServerConnections from '../ServerConnections'; +import toast from '../toast/toast'; + +/* 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 + const 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) { + toast(globalize.translate('SettingsSaved')); + } + Events.trigger(instance, 'saved'); + }, () => { + loading.hide(); + }); + }); + } + + function onSubmit(e) { + const self = this; + const apiClient = ServerConnections.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('./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 = ServerConnections.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 b8ab1a9ba5..1b9bf00376 100644 --- a/src/components/displaysettings/displaysettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -1,5 +1,4 @@
-

${Display}

@@ -123,43 +122,47 @@
${LabelPleaseRestart}
-
- -
-
-
- -
-
-
- -
- -
+
${LabelLibraryPageSizeHelp}
-
+
-
${EnableFastImageFadeInHelp}
+
${EnableFasterAnimationsHelp}
+
+ +
+ +
${EnableBlurHashHelp}
+
+ +
+ +
${EnableDetailsBannerHelp}
${EnableBackdropsHelp}
@@ -167,7 +170,7 @@
${EnableThemeSongsHelp}
@@ -175,18 +178,11 @@
${EnableThemeVideosHelp}
-
- -
-