diff --git a/.eslintrc.js b/.eslintrc.js index 01714c04c4..c089d3ac0e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,8 +2,9 @@ const restrictedGlobals = require('confusing-browser-globals'); module.exports = { root: true, + parser: '@typescript-eslint/parser', plugins: [ - '@babel', + '@typescript-eslint', 'react', 'promise', 'import', @@ -16,14 +17,6 @@ module.exports = { es2017: true, es2020: true }, - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', - ecmaFeatures: { - impliedStrict: true, - jsx: true - } - }, extends: [ 'eslint:recommended', 'plugin:react/recommended', @@ -53,14 +46,19 @@ module.exports = { 'no-multi-spaces': ['error'], 'no-multiple-empty-lines': ['error', { 'max': 1 }], 'no-nested-ternary': ['error'], + 'no-redeclare': ['off'], + '@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }], 'no-restricted-globals': ['error'].concat(restrictedGlobals), 'no-return-assign': ['error'], 'no-return-await': ['error'], 'no-sequences': ['error', { 'allowInParentheses': false }], - 'no-shadow': ['error'], + 'no-shadow': ['off'], + '@typescript-eslint/no-shadow': ['error'], 'no-trailing-spaces': ['error'], - '@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], - 'no-useless-constructor': ['error'], + 'no-unused-expressions': ['off'], + '@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], + 'no-useless-constructor': ['off'], + '@typescript-eslint/no-useless-constructor': ['error'], 'no-var': ['error'], 'no-void': ['error', { 'allowAsStatement': true }], 'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }], @@ -69,9 +67,10 @@ module.exports = { 'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }], 'padded-blocks': ['error', 'never'], 'prefer-const': ['error', { 'destructuring': 'all' }], + '@typescript-eslint/prefer-for-of': ['error'], 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], 'radix': ['error'], - '@babel/semi': ['error'], + '@typescript-eslint/semi': ['error'], 'space-before-blocks': ['error'], 'space-infix-ops': 'error', 'yoda': 'error', @@ -201,9 +200,12 @@ module.exports = { files: [ './src/**/*.js', './src/**/*.jsx', - './src/**/*.ts' + './src/**/*.ts', + './src/**/*.tsx' ], - parser: '@babel/eslint-parser', + parserOptions: { + project: ['./tsconfig.json'] + }, env: { node: false, amd: true, @@ -243,6 +245,7 @@ module.exports = { 'Windows': 'readonly' }, rules: { + '@typescript-eslint/no-floating-promises': ['warn'] } }, // TypeScript source files @@ -251,8 +254,6 @@ module.exports = { './src/**/*.ts', './src/**/*.tsx' ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], extends: [ 'eslint:recommended', 'plugin:import/typescript', @@ -263,13 +264,9 @@ module.exports = { 'plugin:jsx-a11y/recommended' ], rules: { - // Use TypeScript equivalent rules when required - 'no-shadow': ['off'], - '@typescript-eslint/no-shadow': ['error'], - 'no-useless-constructor': ['off'], - '@typescript-eslint/no-useless-constructor': ['error'], + '@typescript-eslint/no-floating-promises': ['error'], - 'sonarjs/cognitive-complexity': ['warn'] + 'sonarjs/cognitive-complexity': ['error'] } } ] diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 76f544d9fd..696c591aa5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,11 +21,11 @@ jobs: - name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Initialize CodeQL - uses: github/codeql-action/init@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 + uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 20ec075c5b..27bf335e19 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -28,7 +28,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} - name: Comment on failure if: failure() - uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0 + uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 with: token: ${{ secrets.JF_BOT_TOKEN }} issue-number: ${{ github.event.issue.number }} diff --git a/README.md b/README.md index 99637125ae..bcb0c53311 100644 --- a/README.md +++ b/README.md @@ -76,21 +76,24 @@ Jellyfin Web is the frontend used for most of the clients available for end user ``` . └── src - ├── assets # Static assets - ├── components # Higher order visual components and React components - ├── controllers # Legacy page views and controllers 🧹 - ├── elements # Basic webcomponents and React wrappers 🧹 - ├── hooks # Custom React hooks - ├── legacy # Polyfills for legacy browsers - ├── libraries # Third party libraries 🧹 - ├── plugins # Client plugins - ├── routes # React routes/pages - ├── scripts # Random assortment of visual components and utilities 🐉 - ├── strings # Translation files - ├── styles # Common app Sass stylesheets - ├── themes # CSS themes - ├── types # Common TypeScript interfaces/types - └── utils # Utility functions + ├── apps + │   ├── experimental # New experimental app layout + │   └── stable # Classic (stable) app layout + ├── assets # Static assets + ├── components # Higher order visual components and React components + ├── controllers # Legacy page views and controllers 🧹 + ├── elements # Basic webcomponents and React wrappers 🧹 + ├── hooks # Custom React hooks + ├── legacy # Polyfills for legacy browsers + ├── libraries # Third party libraries 🧹 + ├── plugins # Client plugins + ├── routes # React routes/pages + ├── scripts # Random assortment of visual components and utilities 🐉 + ├── strings # Translation files + ├── styles # Common app Sass stylesheets + ├── themes # CSS themes + ├── types # Common TypeScript interfaces/types + └── utils # Utility functions ``` - 🧹 — Needs cleanup diff --git a/deployment/Dockerfile.fedora b/deployment/Dockerfile.fedora index 9248c209d7..5e44024f2e 100644 --- a/deployment/Dockerfile.fedora +++ b/deployment/Dockerfile.fedora @@ -1,4 +1,4 @@ -FROM fedora:38 +FROM fedora:39 # Docker build arguments ARG SOURCE_DIR=/jellyfin diff --git a/package-lock.json b/package-lock.json index c2ad0dffb8..96e802f36c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "history": "5.3.0", "hls.js": "1.4.0", "intersection-observer": "0.12.2", - "jassub": "1.5.12", + "jassub": "1.5.13", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.4", "jstree": "3.3.15", @@ -48,7 +48,7 @@ "resize-observer-polyfill": "1.5.1", "screenfull": "6.0.2", "sortablejs": "1.15.0", - "swiper": "8.4.7", + "swiper": "9.2.4", "webcomponents.js": "0.7.24", "whatwg-fetch": "3.6.2", "workbox-core": "6.5.4", @@ -56,8 +56,6 @@ }, "devDependencies": { "@babel/core": "7.21.4", - "@babel/eslint-parser": "7.21.3", - "@babel/eslint-plugin": "7.19.1", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-transform-modules-umd": "7.18.6", @@ -67,9 +65,9 @@ "@types/loadable__component": "5.13.4", "@types/lodash-es": "4.17.7", "@types/react": "17.0.58", - "@types/react-dom": "17.0.19", - "@typescript-eslint/eslint-plugin": "5.58.0", - "@typescript-eslint/parser": "5.58.0", + "@types/react-dom": "17.0.20", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", "@uupaa/dynamic-import-polyfill": "1.0.2", "autoprefixer": "10.4.14", "babel-loader": "9.1.2", @@ -81,7 +79,7 @@ "css-loader": "6.7.3", "cssnano": "6.0.0", "es-check": "7.1.1", - "eslint": "8.38.0", + "eslint": "8.39.0", "eslint-plugin-compat": "4.1.4", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.27.5", @@ -92,17 +90,17 @@ "eslint-plugin-sonarjs": "0.19.0", "expose-loader": "4.1.0", "html-loader": "4.2.0", - "html-webpack-plugin": "5.5.0", + "html-webpack-plugin": "5.5.1", "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.21", + "postcss": "8.4.23", "postcss-loader": "7.2.4", - "postcss-preset-env": "8.3.1", + "postcss-preset-env": "8.3.2", "postcss-scss": "4.0.6", - "sass": "1.62.0", + "sass": "1.62.1", "sass-loader": "13.2.2", "source-map-loader": "4.0.1", "style-loader": "3.3.2", - "stylelint": "15.4.0", + "stylelint": "15.6.0", "stylelint-config-rational-order": "0.1.2", "stylelint-no-browser-hacks": "1.2.1", "stylelint-order": "6.0.3", @@ -111,7 +109,7 @@ "typescript": "5.0.4", "webpack": "5.79.0", "webpack-cli": "5.0.1", - "webpack-dev-server": "4.13.2", + "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0", "workbox-webpack-plugin": "6.5.4", "worker-loader": "3.0.8" @@ -225,40 +223,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/eslint-parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz", - "integrity": "sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==", - "dev": true, - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/@babel/eslint-plugin": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.19.1.tgz", - "integrity": "sha512-ElGPkQPapKMa3zVqXHkZYzuL7I5LbRw9UWBUArgWsdWDDb9XcACqOpBib5tRPA9XvbVZYrFUkoQPbiJ4BFvu4w==", - "dev": true, - "dependencies": { - "eslint-rule-composer": "^0.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/eslint-parser": ">=7.11.0", - "eslint": ">=7.5.0" - } - }, "node_modules/@babel/generator": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", @@ -2667,9 +2631,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2902,15 +2866,6 @@ "node": ">=4" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3304,9 +3259,9 @@ } }, "node_modules/@types/react-dom": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", - "integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz", + "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==", "dev": true, "dependencies": { "@types/react": "^17" @@ -3410,15 +3365,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -3459,14 +3414,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" }, "engines": { @@ -3486,13 +3441,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3503,13 +3458,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -3530,9 +3485,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3543,13 +3498,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3614,17 +3569,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -3655,12 +3610,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -6309,14 +6264,6 @@ } ] }, - "node_modules/dom7": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz", - "integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==", - "dependencies": { - "ssr-window": "^4.0.0" - } - }, "node_modules/domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", @@ -6798,15 +6745,15 @@ } }, "node_modules/eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6816,7 +6763,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -7240,15 +7187,6 @@ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/eslint-rule-composer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7262,15 +7200,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -9054,9 +8983,9 @@ } }, "node_modules/html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true, "engines": { "node": ">=8" @@ -9066,9 +8995,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", + "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -10101,9 +10030,9 @@ } }, "node_modules/jassub": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", - "integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.13.tgz", + "integrity": "sha512-mQM88BcYgppvpPG6VE+DPQm7r6QS65EBedbm13RE4lRIhdrnQ+ihWhBOZXYZe3SlGhg+ROIDRK8uY4dm9ER2XQ==", "dependencies": { "rvfc-polyfill": "^1.0.4" } @@ -11042,10 +10971,16 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -11887,9 +11822,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "funding": [ { @@ -11899,10 +11834,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -13144,16 +13083,26 @@ } }, "node_modules/postcss-preset-env": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.1.tgz", - "integrity": "sha512-k3Y8BXbVLBAufrla3CNmQJhMS1iRuT9LFlysYvzs1rU5E78+ShX2u0EUL6KpMi0pDJO3wZcuVYSR8cgukfoRtg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.2.tgz", + "integrity": "sha512-VSAOsfxTXzO/gX5QljC8x8hN3ABbD9iqqLgqHqohBdNI5FhJptwpl96kpu+kYvvzK7BWwaHYou0IeYrp+NqmcQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { "@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-color-function": "^2.2.1", "@csstools/postcss-color-mix-function": "^1.0.1", "@csstools/postcss-font-format-keywords": "^2.0.2", - "@csstools/postcss-gradients-interpolation-method": "^3.0.3", + "@csstools/postcss-gradients-interpolation-method": "^3.0.4", "@csstools/postcss-hwb-function": "^2.2.1", "@csstools/postcss-ic-unit": "^2.0.2", "@csstools/postcss-is-pseudo-class": "^3.2.0", @@ -13208,10 +13157,6 @@ "engines": { "node": "^14 || ^16 || >=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, "peerDependencies": { "postcss": "^8.4" } @@ -14325,9 +14270,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", - "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -15450,14 +15395,14 @@ } }, "node_modules/stylelint": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.4.0.tgz", - "integrity": "sha512-TlOvpG3MbcFwHmK0q2ykhmpKo7Dq892beJit0NPdpyY9b1tFah/hGhqnAz/bRm2PDhDbJLKvjzkEYYBEz7Dxcg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.0.tgz", + "integrity": "sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg==", "dev": true, "dependencies": { - "@csstools/css-parser-algorithms": "^2.1.0", - "@csstools/css-tokenizer": "^2.1.0", - "@csstools/media-query-list-parser": "^2.0.1", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/media-query-list-parser": "^2.0.4", "@csstools/selector-specificity": "^2.2.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", @@ -15471,7 +15416,7 @@ "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", - "html-tags": "^3.2.0", + "html-tags": "^3.3.1", "ignore": "^5.2.4", "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", @@ -15482,7 +15427,7 @@ "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.21", + "postcss": "^8.4.22", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", @@ -18141,9 +18086,9 @@ } }, "node_modules/swiper": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", - "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.2.4.tgz", + "integrity": "sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==", "funding": [ { "type": "patreon", @@ -18154,9 +18099,7 @@ "url": "http://opencollective.com/swiper" } ], - "hasInstallScript": true, "dependencies": { - "dom7": "^4.0.4", "ssr-window": "^4.0.2" }, "engines": { @@ -19308,9 +19251,9 @@ "dev": true }, "node_modules/webpack-dev-server": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", - "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz", + "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -20165,26 +20108,6 @@ "semver": "^6.3.0" } }, - "@babel/eslint-parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz", - "integrity": "sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==", - "dev": true, - "requires": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - } - }, - "@babel/eslint-plugin": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.19.1.tgz", - "integrity": "sha512-ElGPkQPapKMa3zVqXHkZYzuL7I5LbRw9UWBUArgWsdWDDb9XcACqOpBib5tRPA9XvbVZYrFUkoQPbiJ4BFvu4w==", - "dev": true, - "requires": { - "eslint-rule-composer": "^0.3.0" - } - }, "@babel/generator": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", @@ -21748,9 +21671,9 @@ } }, "@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true }, "@fontsource/noto-sans": { @@ -21932,15 +21855,6 @@ "glob-to-regexp": "^0.3.0" } }, - "@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "requires": { - "eslint-scope": "5.1.1" - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -22292,9 +22206,9 @@ } }, "@types/react-dom": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", - "integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz", + "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==", "dev": true, "requires": { "@types/react": "^17" @@ -22397,15 +22311,15 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", - "integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/type-utils": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -22426,53 +22340,53 @@ } }, "@typescript-eslint/parser": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", - "integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", - "integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0" + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" } }, "@typescript-eslint/type-utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", - "integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.58.0", - "@typescript-eslint/utils": "5.58.0", + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", - "integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", - "integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/visitor-keys": "5.58.0", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -22512,17 +22426,17 @@ } }, "@typescript-eslint/utils": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", - "integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.58.0", - "@typescript-eslint/types": "5.58.0", - "@typescript-eslint/typescript-estree": "5.58.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -22539,12 +22453,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", - "integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.58.0", + "@typescript-eslint/types": "5.59.1", "eslint-visitor-keys": "^3.3.0" }, "dependencies": { @@ -24541,14 +24455,6 @@ } } }, - "dom7": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz", - "integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==", - "requires": { - "ssr-window": "^4.0.0" - } - }, "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", @@ -24933,15 +24839,15 @@ "dev": true }, "eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -24951,7 +24857,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -25454,12 +25360,6 @@ "dev": true, "requires": {} }, - "eslint-rule-composer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true - }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -25470,12 +25370,6 @@ "estraverse": "^4.1.1" } }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, "espree": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", @@ -26647,15 +26541,15 @@ } }, "html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true }, "html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", + "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==", "dev": true, "requires": { "@types/html-minifier-terser": "^6.0.0", @@ -27377,9 +27271,9 @@ "dev": true }, "jassub": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", - "integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.13.tgz", + "integrity": "sha512-mQM88BcYgppvpPG6VE+DPQm7r6QS65EBedbm13RE4lRIhdrnQ+ihWhBOZXYZe3SlGhg+ROIDRK8uY4dm9ER2XQ==", "requires": { "rvfc-polyfill": "^1.0.4" } @@ -28120,9 +28014,9 @@ "optional": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "nanomatch": { @@ -28767,12 +28661,12 @@ "dev": true }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -29565,16 +29459,16 @@ } }, "postcss-preset-env": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.1.tgz", - "integrity": "sha512-k3Y8BXbVLBAufrla3CNmQJhMS1iRuT9LFlysYvzs1rU5E78+ShX2u0EUL6KpMi0pDJO3wZcuVYSR8cgukfoRtg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.2.tgz", + "integrity": "sha512-VSAOsfxTXzO/gX5QljC8x8hN3ABbD9iqqLgqHqohBdNI5FhJptwpl96kpu+kYvvzK7BWwaHYou0IeYrp+NqmcQ==", "dev": true, "requires": { "@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-color-function": "^2.2.1", "@csstools/postcss-color-mix-function": "^1.0.1", "@csstools/postcss-font-format-keywords": "^2.0.2", - "@csstools/postcss-gradients-interpolation-method": "^3.0.3", + "@csstools/postcss-gradients-interpolation-method": "^3.0.4", "@csstools/postcss-hwb-function": "^2.2.1", "@csstools/postcss-ic-unit": "^2.0.2", "@csstools/postcss-is-pseudo-class": "^3.2.0", @@ -30437,9 +30331,9 @@ "dev": true }, "sass": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", - "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -31325,14 +31219,14 @@ } }, "stylelint": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.4.0.tgz", - "integrity": "sha512-TlOvpG3MbcFwHmK0q2ykhmpKo7Dq892beJit0NPdpyY9b1tFah/hGhqnAz/bRm2PDhDbJLKvjzkEYYBEz7Dxcg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.0.tgz", + "integrity": "sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg==", "dev": true, "requires": { - "@csstools/css-parser-algorithms": "^2.1.0", - "@csstools/css-tokenizer": "^2.1.0", - "@csstools/media-query-list-parser": "^2.0.1", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/media-query-list-parser": "^2.0.4", "@csstools/selector-specificity": "^2.2.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", @@ -31346,7 +31240,7 @@ "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", - "html-tags": "^3.2.0", + "html-tags": "^3.3.1", "ignore": "^5.2.4", "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", @@ -31357,7 +31251,7 @@ "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.21", + "postcss": "^8.4.22", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^6.0.0", @@ -33435,11 +33329,10 @@ } }, "swiper": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", - "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.2.4.tgz", + "integrity": "sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==", "requires": { - "dom7": "^4.0.4", "ssr-window": "^4.0.2" } }, @@ -34327,9 +34220,9 @@ } }, "webpack-dev-server": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", - "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz", + "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==", "dev": true, "requires": { "@types/bonjour": "^3.5.9", diff --git a/package.json b/package.json index 823812e8be..efe32ce651 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,6 @@ "license": "GPL-2.0-or-later", "devDependencies": { "@babel/core": "7.21.4", - "@babel/eslint-parser": "7.21.3", - "@babel/eslint-plugin": "7.19.1", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-transform-modules-umd": "7.18.6", @@ -17,9 +15,9 @@ "@types/loadable__component": "5.13.4", "@types/lodash-es": "4.17.7", "@types/react": "17.0.58", - "@types/react-dom": "17.0.19", - "@typescript-eslint/eslint-plugin": "5.58.0", - "@typescript-eslint/parser": "5.58.0", + "@types/react-dom": "17.0.20", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", "@uupaa/dynamic-import-polyfill": "1.0.2", "autoprefixer": "10.4.14", "babel-loader": "9.1.2", @@ -31,7 +29,7 @@ "css-loader": "6.7.3", "cssnano": "6.0.0", "es-check": "7.1.1", - "eslint": "8.38.0", + "eslint": "8.39.0", "eslint-plugin-compat": "4.1.4", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.27.5", @@ -42,17 +40,17 @@ "eslint-plugin-sonarjs": "0.19.0", "expose-loader": "4.1.0", "html-loader": "4.2.0", - "html-webpack-plugin": "5.5.0", + "html-webpack-plugin": "5.5.1", "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.21", + "postcss": "8.4.23", "postcss-loader": "7.2.4", - "postcss-preset-env": "8.3.1", + "postcss-preset-env": "8.3.2", "postcss-scss": "4.0.6", - "sass": "1.62.0", + "sass": "1.62.1", "sass-loader": "13.2.2", "source-map-loader": "4.0.1", "style-loader": "3.3.2", - "stylelint": "15.4.0", + "stylelint": "15.6.0", "stylelint-config-rational-order": "0.1.2", "stylelint-no-browser-hacks": "1.2.1", "stylelint-order": "6.0.3", @@ -61,7 +59,7 @@ "typescript": "5.0.4", "webpack": "5.79.0", "webpack-cli": "5.0.1", - "webpack-dev-server": "4.13.2", + "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0", "workbox-webpack-plugin": "6.5.4", "worker-loader": "3.0.8" @@ -90,7 +88,7 @@ "history": "5.3.0", "hls.js": "1.4.0", "intersection-observer": "0.12.2", - "jassub": "1.5.12", + "jassub": "1.5.13", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.4", "jstree": "3.3.15", @@ -106,7 +104,7 @@ "resize-observer-polyfill": "1.5.1", "screenfull": "6.0.2", "sortablejs": "1.15.0", - "swiper": "8.4.7", + "swiper": "9.2.4", "webcomponents.js": "0.7.24", "whatwg-fetch": "3.6.2", "workbox-core": "6.5.4", diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 1a544723a1..0000000000 --- a/src/App.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { History } from '@remix-run/router'; -import React, { useEffect } from 'react'; - -import { HistoryRouter } from './components/HistoryRouter'; -import { ApiProvider } from './hooks/useApi'; -import { AppRoutes, ExperimentalAppRoutes } from './routes'; - -const App = ({ history }: { history: History }) => { - const layoutMode = localStorage.getItem('layout'); - - useEffect(() => { - Promise.all([ - // Initialize the UI components after first render - import('./scripts/libraryMenu'), - import('./scripts/autoBackdrops') - ]); - }, []); - - return ( - - -
-
- -
-
-
-
- -
-
- {layoutMode === 'experimental' ? : } -
- -
- - - ); -}; - -export default App; diff --git a/src/RootApp.tsx b/src/RootApp.tsx new file mode 100644 index 0000000000..a44e25d565 --- /dev/null +++ b/src/RootApp.tsx @@ -0,0 +1,30 @@ +import loadable from '@loadable/component'; +import { History } from '@remix-run/router'; +import React from 'react'; + +import StableApp from './apps/stable/App'; +import { HistoryRouter } from './components/router/HistoryRouter'; +import { ApiProvider } from './hooks/useApi'; +import { WebConfigProvider } from './hooks/useWebConfig'; + +const ExperimentalApp = loadable(() => import('./apps/experimental/App')); + +const RootApp = ({ history }: { history: History }) => { + const layoutMode = localStorage.getItem('layout'); + + return ( + + + + { + layoutMode === 'experimental' ? + : + + } + + + + ); +}; + +export default RootApp; diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index 33f3752ca7..a9f7f61ccd 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -268,7 +268,7 @@ declare module 'jellyfin-apiclient' { sendWebSocketMessage(name: string, data: any): void; serverAddress(val?: string): string; serverId(): string; - serverVersion(): string + serverVersion(): string; setAuthenticationInfo(accessKey?: string, userId?: string): void; setRequestHeaders(headers: any): void; setSystemInfo(info: SystemInfo): void; diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx new file mode 100644 index 0000000000..842cdaf38a --- /dev/null +++ b/src/apps/experimental/App.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import AppHeader from '../../components/AppHeader'; +import Backdrop from '../../components/Backdrop'; +import { ExperimentalAppRoutes } from './routes/AppRoutes'; + +const ExperimentalApp = () => ( + <> + + + +
+
+ +
+ +); + +export default ExperimentalApp; diff --git a/src/routes/experimentalAppRoutes/index.tsx b/src/apps/experimental/routes/AppRoutes.tsx similarity index 82% rename from src/routes/experimentalAppRoutes/index.tsx rename to src/apps/experimental/routes/AppRoutes.tsx index bd4cf8a19f..e87476c554 100644 --- a/src/routes/experimentalAppRoutes/index.tsx +++ b/src/apps/experimental/routes/AppRoutes.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import ConnectionRequired from '../../components/ConnectionRequired'; -import ServerContentPage from '../../components/ServerContentPage'; -import { toAsyncPageRoute } from '../AsyncRoute'; -import { toViewManagerPageRoute } from '../LegacyRoute'; +import ConnectionRequired from '../../../components/ConnectionRequired'; +import ServerContentPage from '../../../components/ServerContentPage'; +import { toAsyncPageRoute } from '../../../components/router/AsyncRoute'; +import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; diff --git a/src/routes/experimentalAppRoutes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts similarity index 85% rename from src/routes/experimentalAppRoutes/asyncRoutes/admin.ts rename to src/apps/experimental/routes/asyncRoutes/admin.ts index 3c75058288..0f2ec6b9cf 100644 --- a/src/routes/experimentalAppRoutes/asyncRoutes/admin.ts +++ b/src/apps/experimental/routes/asyncRoutes/admin.ts @@ -1,4 +1,4 @@ -import { AsyncRoute } from '../../AsyncRoute'; +import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'usernew.html', page: 'user/usernew' }, diff --git a/src/routes/experimentalAppRoutes/asyncRoutes/index.ts b/src/apps/experimental/routes/asyncRoutes/index.ts similarity index 100% rename from src/routes/experimentalAppRoutes/asyncRoutes/index.ts rename to src/apps/experimental/routes/asyncRoutes/index.ts diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts new file mode 100644 index 0000000000..d7ea0dd7d4 --- /dev/null +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -0,0 +1,8 @@ +import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute'; + +export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'search.html', page: 'search' }, + { path: 'userprofile.html', page: 'user/userprofile' }, + { path: 'home.html', page: 'home', type: AsyncRouteType.Experimental }, + { path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental } +]; diff --git a/src/routes/home.tsx b/src/apps/experimental/routes/home.tsx similarity index 86% rename from src/routes/home.tsx rename to src/apps/experimental/routes/home.tsx index 7fea637cb9..af14e252e1 100644 --- a/src/routes/home.tsx +++ b/src/apps/experimental/routes/home.tsx @@ -1,20 +1,20 @@ import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; -import globalize from '../scripts/globalize'; -import LibraryMenu from '../scripts/libraryMenu'; -import { clearBackdrop } from '../components/backdrop/backdrop'; -import layoutManager from '../components/layoutManager'; -import * as mainTabsManager from '../components/maintabsmanager'; -import '../elements/emby-tabs/emby-tabs'; -import '../elements/emby-button/emby-button'; -import '../elements/emby-scroller/emby-scroller'; -import Page from '../components/Page'; +import globalize from '../../../scripts/globalize'; +import LibraryMenu from '../../../scripts/libraryMenu'; +import { clearBackdrop } from '../../../components/backdrop/backdrop'; +import layoutManager from '../../../components/layoutManager'; +import * as mainTabsManager from '../../../components/maintabsmanager'; +import '../../../elements/emby-tabs/emby-tabs'; +import '../../../elements/emby-button/emby-button'; +import '../../../elements/emby-scroller/emby-scroller'; +import Page from '../../../components/Page'; type OnResumeOptions = { autoFocus?: boolean; refresh?: boolean -} +}; type ControllerProps = { onResume: ( @@ -23,7 +23,7 @@ type ControllerProps = { refreshed: boolean; onPause: () => void; destroy: () => void; -} +}; const Home: FunctionComponent = () => { const [ searchParams ] = useSearchParams(); @@ -65,7 +65,7 @@ const Home: FunctionComponent = () => { depends = 'favorites'; } - return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => { + return import(/* webpackChunkName: "[request]" */ `../../../controllers/${depends}`).then(({ default: controllerFactory }) => { let controller = tabControllers[index]; if (!controller) { @@ -101,6 +101,8 @@ const Home: FunctionComponent = () => { controller.refreshed = true; tabController.current = controller; + }).catch(err => { + console.error('[Home] failed to get tab controller', err); }); }, [ getTabController ]); diff --git a/src/routes/experimentalAppRoutes/legacyRoutes/admin.ts b/src/apps/experimental/routes/legacyRoutes/admin.ts similarity index 98% rename from src/routes/experimentalAppRoutes/legacyRoutes/admin.ts rename to src/apps/experimental/routes/legacyRoutes/admin.ts index 16365e4fc8..dd1f95f6e5 100644 --- a/src/routes/experimentalAppRoutes/legacyRoutes/admin.ts +++ b/src/apps/experimental/routes/legacyRoutes/admin.ts @@ -1,4 +1,4 @@ -import { LegacyRoute } from '../../LegacyRoute'; +import { LegacyRoute } from '../../../../components/router/LegacyRoute'; export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ { diff --git a/src/routes/appRoutes/legacyRoutes/index.ts b/src/apps/experimental/routes/legacyRoutes/index.ts similarity index 100% rename from src/routes/appRoutes/legacyRoutes/index.ts rename to src/apps/experimental/routes/legacyRoutes/index.ts diff --git a/src/routes/appRoutes/legacyRoutes/public.ts b/src/apps/experimental/routes/legacyRoutes/public.ts similarity index 96% rename from src/routes/appRoutes/legacyRoutes/public.ts rename to src/apps/experimental/routes/legacyRoutes/public.ts index 15f8f0e8a9..4fdb6a9395 100644 --- a/src/routes/appRoutes/legacyRoutes/public.ts +++ b/src/apps/experimental/routes/legacyRoutes/public.ts @@ -1,4 +1,4 @@ -import { LegacyRoute } from '../../LegacyRoute'; +import { LegacyRoute } from '../../../../components/router/LegacyRoute'; export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [ { diff --git a/src/routes/experimentalAppRoutes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts similarity index 97% rename from src/routes/experimentalAppRoutes/legacyRoutes/user.ts rename to src/apps/experimental/routes/legacyRoutes/user.ts index cc41c99957..49afc715f1 100644 --- a/src/routes/experimentalAppRoutes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -1,4 +1,4 @@ -import { LegacyRoute } from '../../LegacyRoute'; +import { LegacyRoute } from '../../../../components/router/LegacyRoute'; export const LEGACY_USER_ROUTES: LegacyRoute[] = [ { diff --git a/src/routes/movies/CollectionsView.tsx b/src/apps/experimental/routes/movies/CollectionsView.tsx similarity index 84% rename from src/routes/movies/CollectionsView.tsx rename to src/apps/experimental/routes/movies/CollectionsView.tsx index 6e94ad2b96..b58cc957e5 100644 --- a/src/routes/movies/CollectionsView.tsx +++ b/src/apps/experimental/routes/movies/CollectionsView.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback } from 'react'; -import ViewItemsContainer from '../../components/common/ViewItemsContainer'; -import { LibraryViewProps } from '../../types/interface'; +import ViewItemsContainer from '../../../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const CollectionsView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { diff --git a/src/routes/movies/FavoritesView.tsx b/src/apps/experimental/routes/movies/FavoritesView.tsx similarity index 81% rename from src/routes/movies/FavoritesView.tsx rename to src/apps/experimental/routes/movies/FavoritesView.tsx index cf6969bbb2..bca4227df4 100644 --- a/src/routes/movies/FavoritesView.tsx +++ b/src/apps/experimental/routes/movies/FavoritesView.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback } from 'react'; -import ViewItemsContainer from '../../components/common/ViewItemsContainer'; -import { LibraryViewProps } from '../../types/interface'; +import ViewItemsContainer from '../../../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const FavoritesView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { diff --git a/src/routes/movies/GenresView.tsx b/src/apps/experimental/routes/movies/GenresView.tsx similarity index 77% rename from src/routes/movies/GenresView.tsx rename to src/apps/experimental/routes/movies/GenresView.tsx index 24ae5f79c0..dd1af9efc9 100644 --- a/src/routes/movies/GenresView.tsx +++ b/src/apps/experimental/routes/movies/GenresView.tsx @@ -1,9 +1,9 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useState } from 'react'; -import loading from '../../components/loading/loading'; -import GenresItemsContainer from '../../components/common/GenresItemsContainer'; -import { LibraryViewProps } from '../../types/interface'; +import loading from '../../../../components/loading/loading'; +import GenresItemsContainer from '../../../../components/common/GenresItemsContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const GenresView: FC = ({ topParentId }) => { const [ itemsResult, setItemsResult ] = useState({}); @@ -23,6 +23,8 @@ const GenresView: FC = ({ topParentId }) => { ).then((result) => { setItemsResult(result); loading.hide(); + }).catch(err => { + console.error('[GenresView] failed to fetch genres', err); }); }, [topParentId]); diff --git a/src/routes/movies/MoviesView.tsx b/src/apps/experimental/routes/movies/MoviesView.tsx similarity index 82% rename from src/routes/movies/MoviesView.tsx rename to src/apps/experimental/routes/movies/MoviesView.tsx index 4103eb908c..510ed9e2b2 100644 --- a/src/routes/movies/MoviesView.tsx +++ b/src/apps/experimental/routes/movies/MoviesView.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback } from 'react'; -import ViewItemsContainer from '../../components/common/ViewItemsContainer'; -import { LibraryViewProps } from '../../types/interface'; +import ViewItemsContainer from '../../../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const MoviesView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { diff --git a/src/routes/movies/SuggestionsView.tsx b/src/apps/experimental/routes/movies/SuggestionsView.tsx similarity index 84% rename from src/routes/movies/SuggestionsView.tsx rename to src/apps/experimental/routes/movies/SuggestionsView.tsx index 0d4b944be9..1d468d009b 100644 --- a/src/routes/movies/SuggestionsView.tsx +++ b/src/apps/experimental/routes/movies/SuggestionsView.tsx @@ -1,13 +1,13 @@ import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import dom from '../../scripts/dom'; -import globalize from '../../scripts/globalize'; -import RecommendationContainer from '../../components/common/RecommendationContainer'; -import SectionContainer from '../../components/common/SectionContainer'; -import { LibraryViewProps } from '../../types/interface'; +import layoutManager from '../../../../components/layoutManager'; +import loading from '../../../../components/loading/loading'; +import dom from '../../../../scripts/dom'; +import globalize from '../../../../scripts/globalize'; +import RecommendationContainer from '../../../../components/common/RecommendationContainer'; +import SectionContainer from '../../../../components/common/SectionContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const SuggestionsView: FC = ({ topParentId }) => { const [ latestItems, setLatestItems ] = useState([]); @@ -28,8 +28,10 @@ const SuggestionsView: FC = ({ topParentId }) => { }, [enableScrollX]); const autoFocus = useCallback((page) => { - import('../../components/autoFocuser').then(({ default: autoFocuser }) => { + import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => { autoFocuser.autoFocus(page); + }).catch(err => { + console.error('[SuggestionsView] failed to load data', err); }); }, []); @@ -55,6 +57,8 @@ const SuggestionsView: FC = ({ topParentId }) => { loading.hide(); autoFocus(page); + }).catch(err => { + console.error('[SuggestionsView] failed to fetch items', err); }); }, [autoFocus]); @@ -72,6 +76,8 @@ const SuggestionsView: FC = ({ topParentId }) => { setLatestItems(items); autoFocus(page); + }).catch(err => { + console.error('[SuggestionsView] failed to fetch latest items', err); }); }, [autoFocus]); @@ -95,6 +101,8 @@ const SuggestionsView: FC = ({ topParentId }) => { setRecommendations(result); autoFocus(page); + }).catch(err => { + console.error('[SuggestionsView] failed to fetch recommendations', err); }); }, [autoFocus]); diff --git a/src/routes/movies/TrailersView.tsx b/src/apps/experimental/routes/movies/TrailersView.tsx similarity index 81% rename from src/routes/movies/TrailersView.tsx rename to src/apps/experimental/routes/movies/TrailersView.tsx index 9e11d41b94..55f6189cfc 100644 --- a/src/routes/movies/TrailersView.tsx +++ b/src/apps/experimental/routes/movies/TrailersView.tsx @@ -1,8 +1,8 @@ import React, { FC, useCallback } from 'react'; -import ViewItemsContainer from '../../components/common/ViewItemsContainer'; -import { LibraryViewProps } from '../../types/interface'; +import ViewItemsContainer from '../../../../components/common/ViewItemsContainer'; +import { LibraryViewProps } from '../../../../types/interface'; const TrailersView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { diff --git a/src/routes/movies/index.tsx b/src/apps/experimental/routes/movies/index.tsx similarity index 85% rename from src/routes/movies/index.tsx rename to src/apps/experimental/routes/movies/index.tsx index 9b1405c9e5..64f66368c8 100644 --- a/src/routes/movies/index.tsx +++ b/src/apps/experimental/routes/movies/index.tsx @@ -1,16 +1,16 @@ -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; +import '../../../../elements/emby-scroller/emby-scroller'; +import '../../../../elements/emby-itemscontainer/emby-itemscontainer'; +import '../../../../elements/emby-tabs/emby-tabs'; +import '../../../../elements/emby-button/emby-button'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import Page from '../../components/Page'; -import globalize from '../../scripts/globalize'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as userSettings from '../../scripts/settings/userSettings'; +import * as mainTabsManager from '../../../../components/maintabsmanager'; +import Page from '../../../../components/Page'; +import globalize from '../../../../scripts/globalize'; +import libraryMenu from '../../../../scripts/libraryMenu'; +import * as userSettings from '../../../../scripts/settings/userSettings'; import CollectionsView from './CollectionsView'; import FavoritesView from './FavoritesView'; import GenresView from './GenresView'; @@ -114,6 +114,8 @@ const Movies: FC = () => { window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => { page.setAttribute('data-title', item.Name as string); libraryMenu.setTitle(item.Name); + }).catch(err => { + console.error('[movies] failed to fetch library', err); }); } else { page.setAttribute('data-title', globalize.translate('Movies')); diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx new file mode 100644 index 0000000000..7d619f6230 --- /dev/null +++ b/src/apps/stable/App.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import AppHeader from '../../components/AppHeader'; +import Backdrop from '../../components/Backdrop'; +import { AppRoutes } from './routes/AppRoutes'; + +const StableApp = () => ( + <> + + + +
+
+ +
+ +); + +export default StableApp; diff --git a/src/routes/appRoutes/index.tsx b/src/apps/stable/routes/AppRoutes.tsx similarity index 74% rename from src/routes/appRoutes/index.tsx rename to src/apps/stable/routes/AppRoutes.tsx index 0136dc5531..fb3527da75 100644 --- a/src/routes/appRoutes/index.tsx +++ b/src/apps/stable/routes/AppRoutes.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import ConnectionRequired from '../../components/ConnectionRequired'; -import ServerContentPage from '../../components/ServerContentPage'; -import { toAsyncPageRoute } from '../AsyncRoute'; -import { toViewManagerPageRoute } from '../LegacyRoute'; -import { ASYNC_USER_ROUTES } from './asyncRoutes'; +import ConnectionRequired from '../../../components/ConnectionRequired'; +import ServerContentPage from '../../../components/ServerContentPage'; +import { toAsyncPageRoute } from '../../../components/router/AsyncRoute'; +import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute'; +import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; export const AppRoutes = () => ( @@ -19,6 +19,7 @@ export const AppRoutes = () => ( {/* Admin routes */} }> + {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} { const [ query, setQuery ] = useState(); diff --git a/src/routes/user/useredit.tsx b/src/apps/stable/routes/user/useredit.tsx similarity index 89% rename from src/routes/user/useredit.tsx rename to src/apps/stable/routes/user/useredit.tsx index 67fe055632..c4acdfaaee 100644 --- a/src/routes/user/useredit.tsx +++ b/src/apps/stable/routes/user/useredit.tsx @@ -1,28 +1,43 @@ import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; -import Dashboard from '../../utils/dashboard'; -import globalize from '../../scripts/globalize'; -import LibraryMenu from '../../scripts/libraryMenu'; -import ButtonElement from '../../elements/ButtonElement'; -import CheckBoxElement from '../../elements/CheckBoxElement'; -import InputElement from '../../elements/InputElement'; -import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import SectionTabs from '../../components/dashboard/users/SectionTabs'; -import loading from '../../components/loading/loading'; -import toast from '../../components/toast/toast'; -import { getParameterByName } from '../../utils/url'; import escapeHTML from 'escape-html'; -import SelectElement from '../../elements/SelectElement'; -import Page from '../../components/Page'; + +import Dashboard from '../../../../utils/dashboard'; +import globalize from '../../../../scripts/globalize'; +import LibraryMenu from '../../../../scripts/libraryMenu'; +import ButtonElement from '../../../../elements/ButtonElement'; +import CheckBoxElement from '../../../../elements/CheckBoxElement'; +import InputElement from '../../../../elements/InputElement'; +import LinkEditUserPreferences from '../../../../components/dashboard/users/LinkEditUserPreferences'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; +import loading from '../../../../components/loading/loading'; +import toast from '../../../../components/toast/toast'; +import { getParameterByName } from '../../../../utils/url'; +import SelectElement from '../../../../elements/SelectElement'; +import Page from '../../../../components/Page'; type ResetProvider = AuthProvider & { checkedAttribute: string -} +}; type AuthProvider = { Name?: string; Id?: string; +}; + +const getCheckedElementDataIds = (elements: NodeListOf) => ( + Array.prototype.filter.call(elements, e => e.checked) + .map(e => e.getAttribute('data-id')) +); + +function onSaveComplete() { + Dashboard.navigate('userprofiles.html') + .catch(err => { + console.error('[useredit] failed to navigate to user profile', err); + }); + loading.hide(); + toast(globalize.translate('SettingsSaved')); } const UserEdit: FunctionComponent = () => { @@ -56,7 +71,7 @@ const UserEdit: FunctionComponent = () => { } const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement; - providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide'); + fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1); setAuthProviders(providers); @@ -73,7 +88,7 @@ const UserEdit: FunctionComponent = () => { } const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement; - providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide'); + fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1); setPasswordResetProviders(providers); @@ -121,6 +136,8 @@ const UserEdit: FunctionComponent = () => { const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement; chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion; triggerChange(chkEnableDeleteAllFolders); + }).catch(err => { + console.error('[useredit] failed to fetch channels', err); }); }, []); @@ -134,18 +151,24 @@ const UserEdit: FunctionComponent = () => { window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) { loadAuthProviders(user, providers); + }).catch(err => { + console.error('[useredit] failed to fetch auth providers', err); }); window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) { loadPasswordResetProviders(user, providers); + }).catch(err => { + console.error('[useredit] failed to fetch password reset providers', err); }); window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { IsHidden: false })).then(function (folders) { loadDeleteFolders(user, folders.Items); + }).catch(err => { + console.error('[useredit] failed to fetch media folders', err); }); const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; - user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide'); + disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled); const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement; txtUserName.disabled = false; @@ -185,6 +208,8 @@ const UserEdit: FunctionComponent = () => { loading.show(); getUser().then(function (user) { loadUser(user); + }).catch(err => { + console.error('[useredit] failed to load data', err); }); }, [loadUser]); @@ -198,19 +223,9 @@ const UserEdit: FunctionComponent = () => { loadData(); - function onSaveComplete() { - Dashboard.navigate('userprofiles.html'); - loading.hide(); - toast(globalize.translate('SettingsSaved')); - } - const saveUser = (user: UserDto) => { - if (!user.Id) { - throw new Error('Unexpected null user.Id'); - } - - if (!user.Policy) { - throw new Error('Unexpected null user.Policy'); + if (!user.Id || !user.Policy) { + throw new Error('Unexpected null user id or policy'); } user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value; @@ -235,18 +250,15 @@ const UserEdit: FunctionComponent = () => { user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value; user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value; user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked; - user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) { - return c.checked; - }).map(function (c) { - return c.getAttribute('data-id'); - }); - if (window.ApiClient.isMinServerVersion('10.6.0')) { - user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; - } - window.ApiClient.updateUser(user).then(function () { - window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () { - onSaveComplete(); - }); + user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder')); + user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; + + window.ApiClient.updateUser(user).then(() => ( + window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}) + )).then(() => { + onSaveComplete(); + }).catch(err => { + console.error('[useredit] failed to update user', err); }); }; @@ -254,6 +266,8 @@ const UserEdit: FunctionComponent = () => { loading.show(); getUser().then(function (result) { saveUser(result); + }).catch(err => { + console.error('[useredit] failed to fetch user', err); }); e.preventDefault(); e.stopPropagation(); @@ -261,16 +275,13 @@ const UserEdit: FunctionComponent = () => { }; (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { - if (this.checked) { - (page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide'); - } else { - (page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide'); - } + (page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked); }); window.ApiClient.getNamedConfiguration('network').then(function (config) { - const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement; - config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide'); + (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess); + }).catch(err => { + console.error('[useredit] failed to load network config', err); }); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); diff --git a/src/routes/user/userlibraryaccess.tsx b/src/apps/stable/routes/user/userlibraryaccess.tsx similarity index 92% rename from src/routes/user/userlibraryaccess.tsx rename to src/apps/stable/routes/user/userlibraryaccess.tsx index 0ca06cee4f..e9af88cb1f 100644 --- a/src/routes/user/userlibraryaccess.tsx +++ b/src/apps/stable/routes/user/userlibraryaccess.tsx @@ -1,24 +1,24 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; -import loading from '../../components/loading/loading'; -import libraryMenu from '../../scripts/libraryMenu'; -import globalize from '../../scripts/globalize'; -import toast from '../../components/toast/toast'; -import SectionTabs from '../../components/dashboard/users/SectionTabs'; -import ButtonElement from '../../elements/ButtonElement'; -import { getParameterByName } from '../../utils/url'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import AccessContainer from '../../components/dashboard/users/AccessContainer'; -import CheckBoxElement from '../../elements/CheckBoxElement'; -import Page from '../../components/Page'; +import loading from '../../../../components/loading/loading'; +import libraryMenu from '../../../../scripts/libraryMenu'; +import globalize from '../../../../scripts/globalize'; +import toast from '../../../../components/toast/toast'; +import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; +import ButtonElement from '../../../../elements/ButtonElement'; +import { getParameterByName } from '../../../../utils/url'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import AccessContainer from '../../../../components/dashboard/users/AccessContainer'; +import CheckBoxElement from '../../../../elements/CheckBoxElement'; +import Page from '../../../../components/Page'; type ItemsArr = { Name?: string; Id?: string; AppName?: string; checkedAttribute?: string -} +}; const UserLibraryAccess: FunctionComponent = () => { const [ userName, setUserName ] = useState(''); @@ -148,6 +148,8 @@ const UserLibraryAccess: FunctionComponent = () => { const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices')); Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) { loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items); + }).catch(err => { + console.error('[userlibraryaccess] failed to load data', err); }); }, [loadUser]); @@ -166,6 +168,8 @@ const UserLibraryAccess: FunctionComponent = () => { const userId = getParameterByName('userId'); window.ApiClient.getUser(userId).then(function (result) { saveUser(result); + }).catch(err => { + console.error('[userlibraryaccess] failed to fetch user', err); }); e.preventDefault(); e.stopPropagation(); @@ -203,6 +207,8 @@ const UserLibraryAccess: FunctionComponent = () => { user.Policy.BlockedMediaFolders = null; window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { onSaveComplete(); + }).catch(err => { + console.error('[userlibraryaccess] failed to update user policy', err); }); }; diff --git a/src/routes/user/usernew.tsx b/src/apps/stable/routes/user/usernew.tsx similarity index 89% rename from src/routes/user/usernew.tsx rename to src/apps/stable/routes/user/usernew.tsx index e3610b07f5..22758500ea 100644 --- a/src/routes/user/usernew.tsx +++ b/src/apps/stable/routes/user/usernew.tsx @@ -1,25 +1,25 @@ import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; -import Dashboard from '../../utils/dashboard'; -import globalize from '../../scripts/globalize'; -import loading from '../../components/loading/loading'; -import toast from '../../components/toast/toast'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import InputElement from '../../elements/InputElement'; -import ButtonElement from '../../elements/ButtonElement'; -import AccessContainer from '../../components/dashboard/users/AccessContainer'; -import CheckBoxElement from '../../elements/CheckBoxElement'; -import Page from '../../components/Page'; +import Dashboard from '../../../../utils/dashboard'; +import globalize from '../../../../scripts/globalize'; +import loading from '../../../../components/loading/loading'; +import toast from '../../../../components/toast/toast'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import InputElement from '../../../../elements/InputElement'; +import ButtonElement from '../../../../elements/ButtonElement'; +import AccessContainer from '../../../../components/dashboard/users/AccessContainer'; +import CheckBoxElement from '../../../../elements/CheckBoxElement'; +import Page from '../../../../components/Page'; type userInput = { Name?: string; Password?: string; -} +}; type ItemsArr = { Name?: string; Id?: string; -} +}; const UserNew: FunctionComponent = () => { const [ channelsItems, setChannelsItems ] = useState([]); @@ -93,6 +93,8 @@ const UserNew: FunctionComponent = () => { loadMediaFolders(responses[0].Items); loadChannels(responses[1].Items); loading.hide(); + }).catch(err => { + console.error('[usernew] failed to load data', err); }); }, [loadChannels, loadMediaFolders]); @@ -111,12 +113,8 @@ const UserNew: FunctionComponent = () => { userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value; userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value; window.ApiClient.createUser(userInput).then(function (user) { - if (!user.Id) { - throw new Error('Unexpected null user.Id'); - } - - if (!user.Policy) { - throw new Error('Unexpected null user.Policy'); + if (!user.Id || !user.Policy) { + throw new Error('Unexpected null user id or policy'); } user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked; @@ -142,7 +140,12 @@ const UserNew: FunctionComponent = () => { } window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - Dashboard.navigate('useredit.html?userId=' + user.Id); + Dashboard.navigate('useredit.html?userId=' + user.Id) + .catch(err => { + console.error('[usernew] failed to navigate to edit user page', err); + }); + }).catch(err => { + console.error('[usernew] failed to update user policy', err); }); }, function () { toast(globalize.translate('ErrorDefault')); diff --git a/src/routes/user/userparentalcontrol.tsx b/src/apps/stable/routes/user/userparentalcontrol.tsx similarity index 88% rename from src/routes/user/userparentalcontrol.tsx rename to src/apps/stable/routes/user/userparentalcontrol.tsx index dcf465a773..bee0fde1c7 100644 --- a/src/routes/user/userparentalcontrol.tsx +++ b/src/apps/stable/routes/user/userparentalcontrol.tsx @@ -1,26 +1,27 @@ import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client'; import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; -import globalize from '../../scripts/globalize'; -import LibraryMenu from '../../scripts/libraryMenu'; -import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList'; -import BlockedTagList from '../../components/dashboard/users/BlockedTagList'; -import ButtonElement from '../../elements/ButtonElement'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import SectionTabs from '../../components/dashboard/users/SectionTabs'; -import loading from '../../components/loading/loading'; -import toast from '../../components/toast/toast'; -import { getParameterByName } from '../../utils/url'; -import CheckBoxElement from '../../elements/CheckBoxElement'; import escapeHTML from 'escape-html'; -import SelectElement from '../../elements/SelectElement'; -import Page from '../../components/Page'; + +import globalize from '../../../../scripts/globalize'; +import LibraryMenu from '../../../../scripts/libraryMenu'; +import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList'; +import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList'; +import ButtonElement from '../../../../elements/ButtonElement'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; +import loading from '../../../../components/loading/loading'; +import toast from '../../../../components/toast/toast'; +import { getParameterByName } from '../../../../utils/url'; +import CheckBoxElement from '../../../../elements/CheckBoxElement'; +import SelectElement from '../../../../elements/SelectElement'; +import Page from '../../../../components/Page'; type UnratedItem = { name: string; value: string; checkedAttribute: string -} +}; const UserParentalControl: FunctionComponent = () => { const [ userName, setUserName ] = useState(''); @@ -196,6 +197,8 @@ const UserParentalControl: FunctionComponent = () => { const promise2 = window.ApiClient.getParentalRatings(); Promise.all([promise1, promise2]).then(function (responses) { loadUser(responses[0], responses[1]); + }).catch(err => { + console.error('[userparentalcontrol] failed to load data', err); }); }, [loadUser]); @@ -215,12 +218,8 @@ const UserParentalControl: FunctionComponent = () => { }; const saveUser = (user: UserDto) => { - if (!user.Id) { - throw new Error('Unexpected null user.Id'); - } - - if (!user.Policy) { - throw new Error('Unexpected null user.Policy'); + if (!user.Id || !user.Policy) { + throw new Error('Unexpected null user id or policy'); } const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); @@ -234,12 +233,14 @@ const UserParentalControl: FunctionComponent = () => { user.Policy.BlockedTags = getBlockedTagsFromPage(); window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { onSaveComplete(); + }).catch(err => { + console.error('[userparentalcontrol] failed to update user policy', err); }); }; const showSchedulePopup = (schedule: AccessSchedule, index: number) => { schedule = schedule || {}; - import('../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { + import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { accessschedule.show({ schedule: schedule }).then(function (updatedSchedule) { @@ -251,7 +252,11 @@ const UserParentalControl: FunctionComponent = () => { schedules[index] = updatedSchedule; renderAccessSchedule(schedules); + }).catch(() => { + // access schedule closed }); + }).catch(err => { + console.error('[userparentalcontrol] failed to load access schedule', err); }); }; @@ -272,7 +277,7 @@ const UserParentalControl: FunctionComponent = () => { }; const showBlockedTagPopup = () => { - import('../../components/prompt/prompt').then(({ default: prompt }) => { + import('../../../../components/prompt/prompt').then(({ default: prompt }) => { prompt({ label: globalize.translate('LabelTag') }).then(function (value) { @@ -282,7 +287,11 @@ const UserParentalControl: FunctionComponent = () => { tags.push(value); loadBlockedTags(tags); } + }).catch(() => { + // prompt closed }); + }).catch(err => { + console.error('[userparentalcontrol] failed to load prompt', err); }); }; @@ -291,6 +300,8 @@ const UserParentalControl: FunctionComponent = () => { const userId = getParameterByName('userId'); window.ApiClient.getUser(userId).then(function (result) { saveUser(result); + }).catch(err => { + console.error('[userparentalcontrol] failed to fetch user', err); }); e.preventDefault(); e.stopPropagation(); diff --git a/src/routes/user/userpassword.tsx b/src/apps/stable/routes/user/userpassword.tsx similarity index 72% rename from src/routes/user/userpassword.tsx rename to src/apps/stable/routes/user/userpassword.tsx index 5da2a2b5ef..544e8c6dec 100644 --- a/src/routes/user/userpassword.tsx +++ b/src/apps/stable/routes/user/userpassword.tsx @@ -1,10 +1,11 @@ import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; -import SectionTabs from '../../components/dashboard/users/SectionTabs'; -import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm'; -import { getParameterByName } from '../../utils/url'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import Page from '../../components/Page'; -import loading from '../../components/loading/loading'; + +import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; +import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; +import { getParameterByName } from '../../../../utils/url'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import Page from '../../../../components/Page'; +import loading from '../../../../components/loading/loading'; const UserPassword: FunctionComponent = () => { const userId = getParameterByName('userId'); @@ -18,6 +19,8 @@ const UserPassword: FunctionComponent = () => { } setUserName(user.Name); loading.hide(); + }).catch(err => { + console.error('[userpassword] failed to fetch user', err); }); }, [userId]); useEffect(() => { diff --git a/src/routes/user/userprofile.tsx b/src/apps/stable/routes/user/userprofile.tsx similarity index 85% rename from src/routes/user/userprofile.tsx rename to src/apps/stable/routes/user/userprofile.tsx index fb1b5d08f3..0c038bc84a 100644 --- a/src/routes/user/userprofile.tsx +++ b/src/apps/stable/routes/user/userprofile.tsx @@ -2,17 +2,17 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react'; -import Dashboard from '../../utils/dashboard'; -import globalize from '../../scripts/globalize'; -import LibraryMenu from '../../scripts/libraryMenu'; -import { appHost } from '../../components/apphost'; -import confirm from '../../components/confirm/confirm'; -import ButtonElement from '../../elements/ButtonElement'; -import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm'; -import loading from '../../components/loading/loading'; -import toast from '../../components/toast/toast'; -import { getParameterByName } from '../../utils/url'; -import Page from '../../components/Page'; +import Dashboard from '../../../../utils/dashboard'; +import globalize from '../../../../scripts/globalize'; +import LibraryMenu from '../../../../scripts/libraryMenu'; +import { appHost } from '../../../../components/apphost'; +import confirm from '../../../../components/confirm/confirm'; +import ButtonElement from '../../../../elements/ButtonElement'; +import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; +import loading from '../../../../components/loading/loading'; +import toast from '../../../../components/toast/toast'; +import { getParameterByName } from '../../../../utils/url'; +import Page from '../../../../components/Page'; const UserProfile: FunctionComponent = () => { const userId = getParameterByName('userId'); @@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => { loading.show(); window.ApiClient.getUser(userId).then(function (user) { - if (!user.Name) { - throw new Error('Unexpected null user.Name'); - } - - if (!user.Id) { - throw new Error('Unexpected null user.Id'); + if (!user.Name || !user.Id) { + throw new Error('Unexpected null user name or id'); } setUserName(user.Name); @@ -63,8 +59,12 @@ const UserProfile: FunctionComponent = () => { (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide'); } + }).catch(err => { + console.error('[userprofile] failed to get current user', err); }); loading.hide(); + }).catch(err => { + console.error('[userprofile] failed to load data', err); }); }, [userId]); @@ -114,6 +114,8 @@ const UserProfile: FunctionComponent = () => { window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () { loading.hide(); reloadUser(); + }).catch(err => { + console.error('[userprofile] failed to upload image', err); }); }; @@ -129,7 +131,11 @@ const UserProfile: FunctionComponent = () => { window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () { loading.hide(); reloadUser(); + }).catch(err => { + console.error('[userprofile] failed to delete image', err); }); + }).catch(() => { + // confirm dialog closed }); }); diff --git a/src/routes/user/userprofiles.tsx b/src/apps/stable/routes/user/userprofiles.tsx similarity index 67% rename from src/routes/user/userprofiles.tsx rename to src/apps/stable/routes/user/userprofiles.tsx index 055afb0f8b..b386533f4b 100644 --- a/src/routes/user/userprofiles.tsx +++ b/src/apps/stable/routes/user/userprofiles.tsx @@ -1,24 +1,25 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FunctionComponent, useEffect, useState, useRef } from 'react'; -import Dashboard from '../../utils/dashboard'; -import globalize from '../../scripts/globalize'; -import loading from '../../components/loading/loading'; -import dom from '../../scripts/dom'; -import confirm from '../../components/confirm/confirm'; -import UserCardBox from '../../components/dashboard/users/UserCardBox'; -import SectionTitleContainer from '../../elements/SectionTitleContainer'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-button/paper-icon-button-light'; -import '../../components/cardbuilder/card.scss'; -import '../../components/indicators/indicators.scss'; -import '../../styles/flexstyles.scss'; -import Page from '../../components/Page'; + +import Dashboard from '../../../../utils/dashboard'; +import globalize from '../../../../scripts/globalize'; +import loading from '../../../../components/loading/loading'; +import dom from '../../../../scripts/dom'; +import confirm from '../../../../components/confirm/confirm'; +import UserCardBox from '../../../../components/dashboard/users/UserCardBox'; +import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; +import '../../../../elements/emby-button/emby-button'; +import '../../../../elements/emby-button/paper-icon-button-light'; +import '../../../../components/cardbuilder/card.scss'; +import '../../../../components/indicators/indicators.scss'; +import '../../../../styles/flexstyles.scss'; +import Page from '../../../../components/Page'; type MenuEntry = { name?: string; id?: string; icon?: string; -} +}; const UserProfiles: FunctionComponent = () => { const [ users, setUsers ] = useState([]); @@ -30,6 +31,8 @@ const UserProfiles: FunctionComponent = () => { window.ApiClient.getUsers().then(function (result) { setUsers(result); loading.hide(); + }).catch(err => { + console.error('[userprofiles] failed to fetch users', err); }); }; @@ -75,29 +78,42 @@ const UserProfiles: FunctionComponent = () => { icon: 'delete' }); - import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => { + import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => { actionsheet.show({ items: menuItems, positionTo: card, callback: function (id: string) { switch (id) { case 'open': - Dashboard.navigate('useredit.html?userId=' + userId); + Dashboard.navigate('useredit.html?userId=' + userId) + .catch(err => { + console.error('[userprofiles] failed to navigate to user edit page', err); + }); break; case 'access': - Dashboard.navigate('userlibraryaccess.html?userId=' + userId); + Dashboard.navigate('userlibraryaccess.html?userId=' + userId) + .catch(err => { + console.error('[userprofiles] failed to navigate to user library page', err); + }); break; case 'parentalcontrol': - Dashboard.navigate('userparentalcontrol.html?userId=' + userId); + Dashboard.navigate('userparentalcontrol.html?userId=' + userId) + .catch(err => { + console.error('[userprofiles] failed to navigate to parental control page', err); + }); break; case 'delete': deleteUser(userId); } } + }).catch(() => { + // action sheet closed }); + }).catch(err => { + console.error('[userprofiles] failed to load action sheet', err); }); }; @@ -113,7 +129,11 @@ const UserProfiles: FunctionComponent = () => { loading.show(); window.ApiClient.deleteUser(id).then(function () { loadData(); + }).catch(err => { + console.error('[userprofiles] failed to delete user', err); }); + }).catch(() => { + // confirm dialog closed }); }; @@ -126,7 +146,10 @@ const UserProfiles: FunctionComponent = () => { }); (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() { - Dashboard.navigate('usernew.html'); + Dashboard.navigate('usernew.html') + .catch(err => { + console.error('[userprofiles] failed to navigate to new user page', err); + }); }); }, []); diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx new file mode 100644 index 0000000000..941d36a940 --- /dev/null +++ b/src/components/AppHeader.tsx @@ -0,0 +1,20 @@ +import React, { useEffect } from 'react'; + +const AppHeader = () => { + useEffect(() => { + // Initialize the UI components after first render + import('../scripts/libraryMenu'); + }, []); + + return ( + <> +
+
+
+
+
+ + ); +}; + +export default AppHeader; diff --git a/src/components/AsyncPage.tsx b/src/components/AsyncPage.tsx deleted file mode 100644 index 4c1e6fa7a2..0000000000 --- a/src/components/AsyncPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import loadable from '@loadable/component'; - -interface AsyncPageProps { - /** The relative path to the page component in the routes directory. */ - page: string -} - -/** - * Page component that uses the loadable component library to load pages defined in the routes directory asynchronously - * with code splitting. - */ -const AsyncPage = loadable( - (props: AsyncPageProps) => import(/* webpackChunkName: "[request]" */ `../routes/${props.page}`), - { cacheKey: (props: AsyncPageProps) => props.page } -); - -export default AsyncPage; diff --git a/src/components/Backdrop.tsx b/src/components/Backdrop.tsx new file mode 100644 index 0000000000..1283be0aa9 --- /dev/null +++ b/src/components/Backdrop.tsx @@ -0,0 +1,17 @@ +import React, { useEffect } from 'react'; + +const Backdrop = () => { + useEffect(() => { + // Initialize the UI components after first render + import('../scripts/autoBackdrops'); + }, []); + + return ( + <> +
+
+ + ); +}; + +export default Backdrop; diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index 9c5f402b0a..ca6c26b091 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -1,9 +1,9 @@ -import React, { FunctionComponent, useEffect, useState } from 'react'; +import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import type { ConnectResponse } from 'jellyfin-apiclient'; import alert from './alert'; -import { appRouter } from './appRouter'; +import { appRouter } from './router/appRouter'; import Loading from './loading/LoadingComponent'; import ServerConnections from './ServerConnections'; import globalize from '../scripts/globalize'; @@ -35,116 +35,134 @@ const ConnectionRequired: FunctionComponent = ({ const [ isLoading, setIsLoading ] = useState(true); - useEffect(() => { - const bounce = async (connectionResponse: ConnectResponse) => { - switch (connectionResponse.State) { - case ConnectionState.SignedIn: - // Already logged in, bounce to the home page - console.debug('[ConnectionRequired] already logged in, redirecting to home'); - navigate(BounceRoutes.Home); + const bounce = useCallback(async (connectionResponse: ConnectResponse) => { + switch (connectionResponse.State) { + case ConnectionState.SignedIn: + // Already logged in, bounce to the home page + console.debug('[ConnectionRequired] already logged in, redirecting to home'); + navigate(BounceRoutes.Home); + return; + case ConnectionState.ServerSignIn: + // Bounce to the login page + if (location.pathname === BounceRoutes.Login) { + setIsLoading(false); + } else { + console.debug('[ConnectionRequired] not logged in, redirecting to login page'); + navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`); + } + return; + case ConnectionState.ServerSelection: + // Bounce to select server page + console.debug('[ConnectionRequired] redirecting to select server page'); + navigate(BounceRoutes.SelectServer); + return; + case ConnectionState.ServerUpdateNeeded: + // Show update needed message and bounce to select server page + try { + await alert({ + text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'), + html: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin') + }); + } catch (ex) { + console.warn('[ConnectionRequired] failed to show alert', ex); + } + console.debug('[ConnectionRequired] server update required, redirecting to select server page'); + navigate(BounceRoutes.SelectServer); + return; + } + + console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State); + }, [location.pathname, navigate]); + + const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => { + if (firstConnection.State === ConnectionState.ServerSignIn) { + // Verify the wizard is complete + try { + const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`); + if (!infoResponse.ok) { + throw new Error('Public system info request failed'); + } + const systemInfo = await infoResponse.json(); + if (!systemInfo?.StartupWizardCompleted) { + // Update the current ApiClient + // TODO: Is there a better place to handle this? + ServerConnections.setLocalApiClient(firstConnection.ApiClient); + // Bounce to the wizard + console.info('[ConnectionRequired] startup wizard is not complete, redirecting there'); + navigate(BounceRoutes.StartWizard); return; - case ConnectionState.ServerSignIn: - // Bounce to the login page - if (location.pathname === BounceRoutes.Login) { - setIsLoading(false); - } else { - console.debug('[ConnectionRequired] not logged in, redirecting to login page'); - navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`); - } - return; - case ConnectionState.ServerSelection: - // Bounce to select server page - console.debug('[ConnectionRequired] redirecting to select server page'); - navigate(BounceRoutes.SelectServer); - return; - case ConnectionState.ServerUpdateNeeded: - // Show update needed message and bounce to select server page - try { - await alert({ - text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'), - html: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin') + } + } catch (ex) { + console.error('[ConnectionRequired] checking wizard status failed', ex); + return; + } + } + + // Bounce to the correct page in the login flow + bounce(firstConnection) + .catch(err => { + console.error('[ConnectionRequired] failed to bounce', err); + }); + }, [bounce, navigate]); + + const validateUserAccess = useCallback(async () => { + const client = ServerConnections.currentApiClient(); + + // If this is a user route, ensure a user is logged in + if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) { + try { + console.warn('[ConnectionRequired] unauthenticated user attempted to access user route'); + bounce(await ServerConnections.connect()) + .catch(err => { + console.error('[ConnectionRequired] failed to bounce', err); + }); + } catch (ex) { + console.warn('[ConnectionRequired] error bouncing from user route', ex); + } + return; + } + + // If this is an admin route, ensure the user has access + if (isAdminRequired) { + try { + const user = await client?.getCurrentUser(); + if (!user?.Policy?.IsAdministrator) { + console.warn('[ConnectionRequired] normal user attempted to access admin route'); + bounce(await ServerConnections.connect()) + .catch(err => { + console.error('[ConnectionRequired] failed to bounce', err); }); - } catch (ex) { - console.warn('[ConnectionRequired] failed to show alert', ex); - } - console.debug('[ConnectionRequired] server update required, redirecting to select server page'); - navigate(BounceRoutes.SelectServer); - return; - } - - console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State); - }; - - const validateConnection = async () => { - // Check connection status on initial page load - const firstConnection = appRouter.firstConnectionResult; - appRouter.firstConnectionResult = null; - - if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) { - if (firstConnection.State === ConnectionState.ServerSignIn) { - // Verify the wizard is complete - try { - const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`); - if (!infoResponse.ok) { - throw new Error('Public system info request failed'); - } - const systemInfo = await infoResponse.json(); - if (!systemInfo?.StartupWizardCompleted) { - // Update the current ApiClient - // TODO: Is there a better place to handle this? - ServerConnections.setLocalApiClient(firstConnection.ApiClient); - // Bounce to the wizard - console.info('[ConnectionRequired] startup wizard is not complete, redirecting there'); - navigate(BounceRoutes.StartWizard); - return; - } - } catch (ex) { - console.error('[ConnectionRequired] checking wizard status failed', ex); - return; - } - } - - // Bounce to the correct page in the login flow - bounce(firstConnection); - return; - } - - // TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route. - // This case will need to be handled elsewhere before appRouter can be killed. - - const client = ServerConnections.currentApiClient(); - - // If this is a user route, ensure a user is logged in - if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) { - try { - console.warn('[ConnectionRequired] unauthenticated user attempted to access user route'); - bounce(await ServerConnections.connect()); - } catch (ex) { - console.warn('[ConnectionRequired] error bouncing from user route', ex); - } - return; - } - - // If this is an admin route, ensure the user has access - if (isAdminRequired) { - try { - const user = await client?.getCurrentUser(); - if (!user?.Policy?.IsAdministrator) { - console.warn('[ConnectionRequired] normal user attempted to access admin route'); - bounce(await ServerConnections.connect()); - return; - } - } catch (ex) { - console.warn('[ConnectionRequired] error bouncing from admin route', ex); return; } + } catch (ex) { + console.warn('[ConnectionRequired] error bouncing from admin route', ex); + return; } + } - setIsLoading(false); - }; + setIsLoading(false); + }, [bounce, isAdminRequired, isUserRequired]); - validateConnection(); - }, [ isAdminRequired, isUserRequired, location.pathname, navigate ]); + useEffect(() => { + // TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route. + // This case will need to be handled elsewhere before appRouter can be killed. + + // Check connection status on initial page load + const firstConnection = appRouter.firstConnectionResult; + appRouter.firstConnectionResult = null; + + if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) { + handleIncompleteWizard(firstConnection) + .catch(err => { + console.error('[ConnectionRequired] failed to start wizard', err); + }); + } else { + validateUserAccess() + .catch(err => { + console.error('[ConnectionRequired] failed to validate user access', err); + }); + } + }, [handleIncompleteWizard, validateUserAccess]); if (isLoading) { return ; diff --git a/src/components/ServerContentPage.tsx b/src/components/ServerContentPage.tsx index 7473c856e8..5b86d66072 100644 --- a/src/components/ServerContentPage.tsx +++ b/src/components/ServerContentPage.tsx @@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom'; import ServerConnections from './ServerConnections'; import viewManager from './viewManager/viewManager'; import globalize from '../scripts/globalize'; +import type { RestoreViewFailResponse } from '../types/viewManager'; interface ServerContentPageProps { view: string @@ -29,7 +30,7 @@ const ServerContentPage: FunctionComponent = ({ view }) }; viewManager.tryRestoreView(viewOptions) - .catch(async (result?: any) => { + .catch(async (result?: RestoreViewFailResponse) => { if (!result || !result.cancelled) { const apiClient = ServerConnections.currentApiClient(); @@ -46,12 +47,13 @@ const ServerContentPage: FunctionComponent = ({ view }) }; loadPage(); - }, [ + }, + // location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same + // eslint-disable-next-line react-hooks/exhaustive-deps + [ view, location.pathname, location.search - // location.state is NOT included as a dependency here since dialogs will update state while the current view - // stays the same ]); return <>; diff --git a/src/components/alert.js b/src/components/alert.js index bd6ff7187c..6e654e9f3c 100644 --- a/src/components/alert.js +++ b/src/components/alert.js @@ -1,4 +1,4 @@ -import { appRouter } from './appRouter'; +import { appRouter } from './router/appRouter'; import browser from '../scripts/browser'; import dialog from './dialog/dialog'; import globalize from '../scripts/globalize'; diff --git a/src/components/cardbuilder/card.scss b/src/components/cardbuilder/card.scss index e43b488dbe..28f55abe2d 100644 --- a/src/components/cardbuilder/card.scss +++ b/src/components/cardbuilder/card.scss @@ -114,7 +114,7 @@ button::-moz-focus-inner { } .card.show-animation:focus > .cardBox { - transform: scale(1.18, 1.18); + transform: scale(1.07, 1.07); } .cardBox-bottompadded { diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index d4f3aafa49..5ad759002d 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -22,7 +22,7 @@ import './card.scss'; import '../../elements/emby-button/paper-icon-button-light'; import '../guide/programs.scss'; import ServerConnections from '../ServerConnections'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -679,9 +679,8 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout let valid = 0; - for (let i = 0; i < lines.length; i++) { + for (const text of lines) { let currentCssClass = cssClass; - const text = lines[i]; if (valid > 0 && isOuterFooter) { currentCssClass += ' cardText-secondary'; @@ -862,8 +861,8 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, if (options.textLines) { const additionalLines = options.textLines(item); - for (let i = 0; i < additionalLines.length; i++) { - lines.push(additionalLines[i]); + for (const additionalLine of additionalLines) { + lines.push(additionalLine); } } @@ -1118,7 +1117,6 @@ let refreshIndicatorLoaded; function importRefreshIndicator() { if (!refreshIndicatorLoaded) { refreshIndicatorLoaded = true; - /* eslint-disable-next-line @babel/no-unused-expressions */ import('../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator'); } } @@ -1469,7 +1467,6 @@ function getHoverMenuHtml(item, action) { const userData = item.UserData || {}; if (itemHelper.canMarkPlayed(item)) { - /* eslint-disable-next-line @babel/no-unused-expressions */ import('../../elements/emby-playstatebutton/emby-playstatebutton'); html += ''; } @@ -1477,7 +1474,6 @@ function getHoverMenuHtml(item, action) { if (itemHelper.canRate(item)) { const likes = userData.Likes == null ? '' : userData.Likes; - /* eslint-disable-next-line @babel/no-unused-expressions */ import('../../elements/emby-ratingbutton/emby-ratingbutton'); html += ''; } @@ -1724,8 +1720,7 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) { export function onTimerCancelled(timerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]'); - for (let i = 0; i < cells.length; i++) { - const cell = cells[i]; + for (const cell of cells) { const icon = cell.querySelector('.timerIndicator'); if (icon) { icon.parentNode.removeChild(icon); @@ -1742,8 +1737,7 @@ export function onTimerCancelled(timerId, itemsContainer) { export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]'); - for (let i = 0; i < cells.length; i++) { - const cell = cells[i]; + for (const cell of cells) { const icon = cell.querySelector('.timerIndicator'); if (icon) { icon.parentNode.removeChild(icon); diff --git a/src/components/collectionEditor/collectionEditor.js b/src/components/collectionEditor/collectionEditor.js index efcd7f86e7..455f5f25ea 100644 --- a/src/components/collectionEditor/collectionEditor.js +++ b/src/components/collectionEditor/collectionEditor.js @@ -3,7 +3,7 @@ import dom from '../../scripts/dom'; import dialogHelper from '../dialogHelper/dialogHelper'; import loading from '../loading/loading'; import layoutManager from '../layoutManager'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import globalize from '../../scripts/globalize'; import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/paper-icon-button-light'; diff --git a/src/components/common/Filter.tsx b/src/components/common/Filter.tsx index c3316df1a2..c3ccdd62f3 100644 --- a/src/components/common/Filter.tsx +++ b/src/components/common/Filter.tsx @@ -32,7 +32,11 @@ const Filter: FC = ({ serverId: window.ApiClient.serverId(), filterMenuOptions: getFilterMenuOptions(), setfilters: setViewQuerySettings + }).catch(() => { + // filter menu closed }); + }).catch(err => { + console.error('[Filter] failed to load filter menu', err); }); }, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]); diff --git a/src/components/common/GenresItemsContainer.tsx b/src/components/common/GenresItemsContainer.tsx index b6a06bb26a..09623e7e57 100644 --- a/src/components/common/GenresItemsContainer.tsx +++ b/src/components/common/GenresItemsContainer.tsx @@ -5,7 +5,7 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client' import escapeHTML from 'escape-html'; import React, { FC, useCallback, useEffect, useRef } from 'react'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import cardBuilder from '../cardbuilder/cardBuilder'; import layoutManager from '../layoutManager'; import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; @@ -73,6 +73,8 @@ const GenresItemsContainer: FC = ({ centerText: true, showYear: true }); + }).catch(err => { + console.error('[GenresItemsContainer] failed to fetch items', err); }); }, [getPortraitShape, topParentId]); diff --git a/src/components/common/NewCollection.tsx b/src/components/common/NewCollection.tsx index 4eadda8e36..837fe85fd3 100644 --- a/src/components/common/NewCollection.tsx +++ b/src/components/common/NewCollection.tsx @@ -12,7 +12,11 @@ const NewCollection: FC = () => { collectionEditor.show({ items: [], serverId: serverId + }).catch(() => { + // closed collection editor }); + }).catch(err => { + console.error('[NewCollection] failed to load collection editor', err); }); }, []); diff --git a/src/components/common/SelectView.tsx b/src/components/common/SelectView.tsx index f718529857..bfb34555b8 100644 --- a/src/components/common/SelectView.tsx +++ b/src/components/common/SelectView.tsx @@ -22,7 +22,11 @@ const SelectView: FC = ({ settings: viewQuerySettings, visibleSettings: getVisibleViewSettings(), setviewsettings: setViewQuerySettings + }).catch(() => { + // view settings closed }); + }).catch(err => { + console.error('[SelectView] failed to load view settings', err); }); }, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]); diff --git a/src/components/common/Shuffle.tsx b/src/components/common/Shuffle.tsx index 7d5b06be4b..093dc74874 100644 --- a/src/components/common/Shuffle.tsx +++ b/src/components/common/Shuffle.tsx @@ -18,6 +18,8 @@ const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { topParentId as string ).then((item) => { playbackManager.shuffle(item); + }).catch(err => { + console.error('[Shuffle] failed to fetch items', err); }); }, [topParentId]); diff --git a/src/components/common/Sort.tsx b/src/components/common/Sort.tsx index 5c5d1b6193..db5cb89956 100644 --- a/src/components/common/Sort.tsx +++ b/src/components/common/Sort.tsx @@ -25,7 +25,11 @@ const Sort: FC = ({ settings: viewQuerySettings, sortOptions: getSortMenuOptions(), setSortValues: setViewQuerySettings + }).catch(() => { + // sort menu closed }); + }).catch(err => { + console.error('[Sort] failed to load sort menu', err); }); }, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]); diff --git a/src/components/common/ViewItemsContainer.tsx b/src/components/common/ViewItemsContainer.tsx index 086d160137..5cbc7aace6 100644 --- a/src/components/common/ViewItemsContainer.tsx +++ b/src/components/common/ViewItemsContainer.tsx @@ -1,4 +1,4 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; +import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import loading from '../loading/loading'; @@ -33,6 +33,41 @@ const getDefaultSortBy = () => { return 'SortName'; }; +const getFields = (viewQuerySettings: ViewQuerySettings) => { + const fields: ItemFields[] = [ + ItemFields.BasicSyncInfo, + ItemFields.MediaSourceCount + ]; + + if (viewQuerySettings.imageType === 'primary') { + fields.push(ItemFields.PrimaryImageAspectRatio); + } + + return fields.join(','); +}; + +const getFilters = (viewQuerySettings: ViewQuerySettings) => { + const filters: ItemFilter[] = []; + + if (viewQuerySettings.IsPlayed) { + filters.push(ItemFilter.IsPlayed); + } + + if (viewQuerySettings.IsUnplayed) { + filters.push(ItemFilter.IsUnplayed); + } + + if (viewQuerySettings.IsFavorite) { + filters.push(ItemFilter.IsFavorite); + } + + if (viewQuerySettings.IsResumable) { + filters.push(ItemFilter.IsResumable); + } + + return filters; +}; + const getVisibleViewSettings = () => { return [ 'showTitle', @@ -228,33 +263,7 @@ const ViewItemsContainer: FC = ({ }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); const getQuery = useCallback(() => { - let fields = 'BasicSyncInfo,MediaSourceCount'; - - if (viewQuerySettings.imageType === 'primary') { - fields += ',PrimaryImageAspectRatio'; - } - - if (viewQuerySettings.showYear) { - fields += ',ProductionYear'; - } - - const queryFilters: string[] = []; - - if (viewQuerySettings.IsPlayed) { - queryFilters.push('IsPlayed'); - } - - if (viewQuerySettings.IsUnplayed) { - queryFilters.push('IsUnplayed'); - } - - if (viewQuerySettings.IsFavorite) { - queryFilters.push('IsFavorite'); - } - - if (viewQuerySettings.IsResumable) { - queryFilters.push('IsResumable'); - } + const queryFilters = getFilters(viewQuerySettings); let queryIsHD; @@ -271,7 +280,7 @@ const ViewItemsContainer: FC = ({ SortOrder: viewQuerySettings.SortOrder, IncludeItemTypes: getItemTypes().join(','), Recursive: true, - Fields: fields, + Fields: getFields(viewQuerySettings), ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', Limit: userSettings.libraryPageSize(undefined) || undefined, @@ -293,28 +302,7 @@ const ViewItemsContainer: FC = ({ ParentId: topParentId }; }, [ - viewQuerySettings.imageType, - viewQuerySettings.showYear, - viewQuerySettings.IsPlayed, - viewQuerySettings.IsUnplayed, - viewQuerySettings.IsFavorite, - viewQuerySettings.IsResumable, - viewQuerySettings.IsHD, - viewQuerySettings.IsSD, - viewQuerySettings.SortBy, - viewQuerySettings.SortOrder, - viewQuerySettings.VideoTypes, - viewQuerySettings.GenreIds, - viewQuerySettings.Is4K, - viewQuerySettings.Is3D, - viewQuerySettings.HasSubtitles, - viewQuerySettings.HasTrailer, - viewQuerySettings.HasSpecialFeature, - viewQuerySettings.HasThemeSong, - viewQuerySettings.HasThemeVideo, - viewQuerySettings.StartIndex, - viewQuerySettings.NameLessThan, - viewQuerySettings.NameStartsWith, + viewQuerySettings, getItemTypes, getBasekey, topParentId @@ -347,9 +335,13 @@ const ViewItemsContainer: FC = ({ import('../../components/autoFocuser').then(({ default: autoFocuser }) => { autoFocuser.autoFocus(page); + }).catch(err => { + console.error('[ViewItemsContainer] failed to load autofocuser', err); }); loading.hide(); setisLoading(true); + }).catch(err => { + console.error('[ViewItemsContainer] failed to fetch data', err); }); }, [fetchData]); diff --git a/src/components/confirm/confirm.js b/src/components/confirm/confirm.js index a96910e16a..7ab5ff219a 100644 --- a/src/components/confirm/confirm.js +++ b/src/components/confirm/confirm.js @@ -1,4 +1,4 @@ -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import browser from '../../scripts/browser'; import dialog from '../dialog/dialog'; import globalize from '../../scripts/globalize'; diff --git a/src/components/dashboard/users/AccessContainer.tsx b/src/components/dashboard/users/AccessContainer.tsx index 32fe9c213d..88727f396a 100644 --- a/src/components/dashboard/users/AccessContainer.tsx +++ b/src/components/dashboard/users/AccessContainer.tsx @@ -12,7 +12,7 @@ type IProps = { listTitle?: string; description?: string; children?: React.ReactNode -} +}; const AccessContainer: FunctionComponent = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => { return ( diff --git a/src/components/dashboard/users/AccessScheduleList.tsx b/src/components/dashboard/users/AccessScheduleList.tsx index 41c55c3621..f1e5affb8c 100644 --- a/src/components/dashboard/users/AccessScheduleList.tsx +++ b/src/components/dashboard/users/AccessScheduleList.tsx @@ -9,7 +9,7 @@ type AccessScheduleListProps = { DayOfWeek?: string; StartHour?: number ; EndHour?: number; -} +}; function getDisplayTime(hours = 0) { let minutes = 0; diff --git a/src/components/dashboard/users/BlockedTagList.tsx b/src/components/dashboard/users/BlockedTagList.tsx index 20e7f88ef2..5158a63e97 100644 --- a/src/components/dashboard/users/BlockedTagList.tsx +++ b/src/components/dashboard/users/BlockedTagList.tsx @@ -3,7 +3,7 @@ import IconButtonElement from '../../../elements/IconButtonElement'; type IProps = { tag?: string; -} +}; const BlockedTagList: FunctionComponent = ({ tag }: IProps) => { return ( diff --git a/src/components/dashboard/users/LinkEditUserPreferences.tsx b/src/components/dashboard/users/LinkEditUserPreferences.tsx index fde63ba514..fd272fd770 100644 --- a/src/components/dashboard/users/LinkEditUserPreferences.tsx +++ b/src/components/dashboard/users/LinkEditUserPreferences.tsx @@ -4,7 +4,7 @@ import globalize from '../../../scripts/globalize'; type IProps = { title?: string; className?: string; -} +}; const createLinkElement = ({ className, title }: IProps) => ({ __html: ` ({ __html: ` { if (lastActivityDate) { diff --git a/src/components/dashboard/users/UserPasswordForm.tsx b/src/components/dashboard/users/UserPasswordForm.tsx index 3d3daa2993..d7801fdb42 100644 --- a/src/components/dashboard/users/UserPasswordForm.tsx +++ b/src/components/dashboard/users/UserPasswordForm.tsx @@ -1,4 +1,3 @@ -import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; import Dashboard from '../../../utils/dashboard'; import globalize from '../../../scripts/globalize'; @@ -12,12 +11,12 @@ import InputElement from '../../../elements/InputElement'; type IProps = { userId: string; -} +}; const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { const element = useRef(null); - const loadUser = useCallback(() => { + const loadUser = useCallback(async () => { const page = element.current; if (!page) { @@ -25,61 +24,50 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { return; } - window.ApiClient.getUser(userId).then(function (user) { - Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) { - if (!user.Policy) { - throw new Error('Unexpected null user.Policy'); - } + const user = await window.ApiClient.getUser(userId); + const loggedInUser = await Dashboard.getCurrentUser(); - if (!user.Configuration) { - throw new Error('Unexpected null user.Configuration'); - } + if (!user.Policy || !user.Configuration) { + throw new Error('Unexpected null user policy or configuration'); + } - LibraryMenu.setTitle(user.Name); + LibraryMenu.setTitle(user.Name); - let showLocalAccessSection = false; + let showLocalAccessSection = false; - if (user.HasConfiguredPassword) { - (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide'); - (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); - showLocalAccessSection = true; - } else { - (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide'); - (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); - } + if (user.HasConfiguredPassword) { + (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide'); + (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); + showLocalAccessSection = true; + } else { + (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide'); + (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); + } - if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) { - (page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide'); - } else { - (page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide'); - } + const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess; + (page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword); + (page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword)); - if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { - (page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide'); - } else { - (page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide'); - } + const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement; + txtEasyPassword.value = ''; - const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement; - txtEasyPassword.value = ''; + if (user.HasConfiguredEasyPassword) { + txtEasyPassword.placeholder = '******'; + (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide'); + } else { + txtEasyPassword.removeAttribute('placeholder'); + txtEasyPassword.placeholder = ''; + (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide'); + } - if (user.HasConfiguredEasyPassword) { - txtEasyPassword.placeholder = '******'; - (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide'); - } else { - txtEasyPassword.removeAttribute('placeholder'); - txtEasyPassword.placeholder = ''; - (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide'); - } + const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement; - const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement; + chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false; - chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false; - - import('../../autoFocuser').then(({ default: autoFocuser }) => { - autoFocuser.autoFocus(page); - }); - }); + import('../../autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }).catch(err => { + console.error('[UserPasswordForm] failed to load autofocuser', err); }); (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = ''; @@ -95,7 +83,9 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { return; } - loadUser(); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); const onSubmit = (e: Event) => { if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) { @@ -123,7 +113,9 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { loading.hide(); toast(globalize.translate('PasswordSaved')); - loadUser(); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); }, function () { loading.hide(); Dashboard.alert({ @@ -146,6 +138,8 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { if (easyPassword) { window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () { onEasyPasswordSaved(); + }).catch(err => { + console.error('[UserPasswordForm] failed to update easy password', err); }); } else { onEasyPasswordSaved(); @@ -167,8 +161,14 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { loading.hide(); toast(globalize.translate('SettingsSaved')); - loadUser(); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); + }).catch(err => { + console.error('[UserPasswordForm] failed to update user configuration', err); }); + }).catch(err => { + console.error('[UserPasswordForm] failed to fetch user', err); }); }; @@ -183,8 +183,14 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { message: globalize.translate('PinCodeResetComplete'), title: globalize.translate('HeaderPinCodeReset') }); - loadUser(); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); + }).catch(err => { + console.error('[UserPasswordForm] failed to reset easy password', err); }); + }).catch(() => { + // confirm dialog was closed }); }; @@ -198,8 +204,14 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { message: globalize.translate('PasswordResetComplete'), title: globalize.translate('ResetPassword') }); - loadUser(); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); + }).catch(err => { + console.error('[UserPasswordForm] failed to reset user password', err); }); + }).catch(() => { + // confirm dialog was closed }); }; diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js index 5819f0a755..b03ff9052c 100644 --- a/src/components/dialogHelper/dialogHelper.js +++ b/src/components/dialogHelper/dialogHelper.js @@ -1,4 +1,4 @@ -import { history } from '../appRouter'; +import { history } from '../router/appRouter'; import focusManager from '../focusManager'; import browser from '../../scripts/browser'; import layoutManager from '../layoutManager'; diff --git a/src/components/groupedcards.js b/src/components/groupedcards.js index a2f7b757e0..9167dc3ec2 100644 --- a/src/components/groupedcards.js +++ b/src/components/groupedcards.js @@ -1,5 +1,5 @@ import dom from '../scripts/dom'; -import { appRouter } from './appRouter'; +import { appRouter } from './router/appRouter'; import Dashboard from '../utils/dashboard'; import ServerConnections from './ServerConnections'; diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 5cf83ced94..d5da017e2d 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -3,7 +3,7 @@ import cardBuilder from '../cardbuilder/cardBuilder'; import layoutManager from '../layoutManager'; import imageLoader from '../images/imageLoader'; import globalize from '../../scripts/globalize'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import imageHelper from '../../scripts/imagehelper'; import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; @@ -100,10 +100,10 @@ export function loadSections(elem, apiClient, user, userSettings) { export function destroySections(elem) { const elems = elem.querySelectorAll('.itemsContainer'); - for (let i = 0; i < elems.length; i++) { - elems[i].fetchData = null; - elems[i].parentContainer = null; - elems[i].getItemsHtml = null; + for (const e of elems) { + e.fetchData = null; + e.parentContainer = null; + e.getItemsHtml = null; } elem.innerHTML = ''; @@ -111,8 +111,8 @@ export function destroySections(elem) { export function pause(elem) { const elems = elem.querySelectorAll('.itemsContainer'); - for (let i = 0; i < elems.length; i++) { - elems[i].pause(); + for (const e of elems) { + e.pause(); } } diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index a90effb336..539d2fc965 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -1,9 +1,10 @@ import browser from '../scripts/browser'; import { copy } from '../scripts/clipboard'; +import dom from '../scripts/dom'; import globalize from '../scripts/globalize'; import actionsheet from './actionSheet/actionSheet'; import { appHost } from './apphost'; -import { appRouter } from './appRouter'; +import { appRouter } from './router/appRouter'; import itemHelper from './itemHelper'; import { playbackManager } from './playback/playbackmanager'; import ServerConnections from './ServerConnections'; @@ -98,6 +99,16 @@ export function getCommands(options) { } if (!browser.tv) { + // Multiselect is currrently only ran on long clicks of card components + // This disables Select on any context menu not originating from a card i.e songs + if (options.positionTo && (dom.parentWithClass(options.positionTo, 'card') !== null)) { + commands.push({ + name: globalize.translate('Select'), + id: 'multiSelect', + icon: 'library_add_check' + }); + } + if (itemHelper.supportsAddingToCollection(item) && options.EnableCollectionManagement) { commands.push({ name: globalize.translate('AddToCollection'), @@ -432,6 +443,12 @@ function executeCommand(item, id, options) { itemMediaInfo.show(itemId, serverId).then(getResolveFunction(resolve, id), getResolveFunction(resolve, id)); }); break; + case 'multiSelect': + import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => { + const card = dom.parentWithClass(options.positionTo, 'card'); + startMultiSelect(card); + }); + break; case 'refresh': refresh(apiClient, item); getResolveFunction(resolve, id)(); diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index ba9f0242f8..55d899921a 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -23,8 +23,7 @@ function populateLanguages(parent) { function populateLanguagesIntoSelect(select, languages) { let html = ''; html += ""; - for (let i = 0; i < languages.length; i++) { - const culture = languages[i]; + for (const culture of languages) { html += ``; } select.innerHTML = html; @@ -32,8 +31,7 @@ function populateLanguagesIntoSelect(select, languages) { function populateLanguagesIntoList(element, languages) { let html = ''; - for (let i = 0; i < languages.length; i++) { - const culture = languages[i]; + for (const culture of languages) { html += ``; } element.innerHTML = html; @@ -43,8 +41,7 @@ function populateCountries(select) { return ApiClient.getCountries().then(allCountries => { let html = ''; html += ""; - for (let i = 0; i < allCountries.length; i++) { - const culture = allCountries[i]; + for (const culture of allCountries) { html += ``; } select.innerHTML = html; @@ -109,8 +106,7 @@ function renderMetadataSavers(page, metadataSavers) { } html += `

${globalize.translate('LabelMetadataSavers')}

`; html += '
'; - for (let i = 0; i < metadataSavers.length; i++) { - const plugin = metadataSavers[i]; + for (const plugin of metadataSavers) { html += ``; } html += '
'; @@ -157,8 +153,7 @@ function getMetadataFetchersForTypeHtml(availableTypeOptions, libraryOptionsForT function getTypeOptions(allOptions, type) { const allTypeOptions = allOptions.TypeOptions || []; - for (let i = 0; i < allTypeOptions.length; i++) { - const typeOptions = allTypeOptions[i]; + for (const typeOptions of allTypeOptions) { if (typeOptions.Type === type) return typeOptions; } return null; @@ -167,8 +162,7 @@ function getTypeOptions(allOptions, type) { function renderMetadataFetchers(page, availableOptions, libraryOptions) { let html = ''; const elem = page.querySelector('.metadataFetchers'); - for (let i = 0; i < availableOptions.TypeOptions.length; i++) { - const availableTypeOptions = availableOptions.TypeOptions[i]; + for (const availableTypeOptions of availableOptions.TypeOptions) { html += getMetadataFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {}); } elem.innerHTML = html; @@ -262,8 +256,7 @@ function getImageFetchersForTypeHtml(availableTypeOptions, libraryOptionsForType function renderImageFetchers(page, availableOptions, libraryOptions) { let html = ''; const elem = page.querySelector('.imageFetchers'); - for (let i = 0; i < availableOptions.TypeOptions.length; i++) { - const availableTypeOptions = availableOptions.TypeOptions[i]; + for (const availableTypeOptions of availableOptions.TypeOptions) { html += getImageFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {}); } elem.innerHTML = html; @@ -460,8 +453,7 @@ function setSubtitleFetchersIntoOptions(parent, options) { function setMetadataFetchersIntoOptions(parent, options) { const sections = parent.querySelectorAll('.metadataFetcher'); - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; + for (const section of sections) { const type = section.getAttribute('data-type'); let typeOptions = getTypeOptions(options, type); if (!typeOptions) { @@ -484,8 +476,7 @@ function setMetadataFetchersIntoOptions(parent, options) { function setImageFetchersIntoOptions(parent, options) { const sections = parent.querySelectorAll('.imageFetcher'); - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; + for (const section of sections) { const type = section.getAttribute('data-type'); let typeOptions = getTypeOptions(options, type); if (!typeOptions) { @@ -509,8 +500,7 @@ function setImageFetchersIntoOptions(parent, options) { function setImageOptionsIntoOptions(options) { const originalTypeOptions = (currentLibraryOptions || {}).TypeOptions || []; - for (let i = 0; i < originalTypeOptions.length; i++) { - const originalTypeOption = originalTypeOptions[i]; + for (const originalTypeOption of originalTypeOptions) { let typeOptions = getTypeOptions(options, originalTypeOption.Type); if (!typeOptions) { diff --git a/src/components/mediainfo/mediainfo.js b/src/components/mediainfo/mediainfo.js index 9f5c366083..ec908d0d0f 100644 --- a/src/components/mediainfo/mediainfo.js +++ b/src/components/mediainfo/mediainfo.js @@ -1,7 +1,7 @@ import escapeHtml from 'escape-html'; import datetime from '../../scripts/datetime'; import globalize from '../../scripts/globalize'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import itemHelper from '../itemHelper'; import indicators from '../indicators/indicators'; import 'material-design-icons-iconfont'; diff --git a/src/components/metadataEditor/metadataEditor.js b/src/components/metadataEditor/metadataEditor.js index 3aeaee63a3..dfe7a99237 100644 --- a/src/components/metadataEditor/metadataEditor.js +++ b/src/components/metadataEditor/metadataEditor.js @@ -20,7 +20,7 @@ import '../../styles/flexstyles.scss'; import './style.scss'; import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import template from './metadataEditor.template.html'; let currentContext; @@ -955,8 +955,7 @@ function populatePeople(context, people) { function getLockedFieldsHtml(fields, currentFields) { let html = ''; - for (let i = 0; i < fields.length; i++) { - const field = fields[i]; + for (const field of fields) { const name = field.name; const value = field.value || field.name; const checkedHtml = currentFields.indexOf(value) === -1 ? ' checked' : ''; diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index 142a31a5d7..989a421cdb 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -564,3 +564,6 @@ export default function (options) { }; } +export const startMultiSelect = (card) => { + showSelections(card); +}; diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index e977f458f9..d0fbd7e6b4 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -15,7 +15,7 @@ import appFooter from '../appFooter/appFooter'; import itemShortcuts from '../shortcuts'; import './nowPlayingBar.scss'; import '../../elements/emby-slider/emby-slider'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; let currentPlayer; let currentPlayerSupportedCommands = []; diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 392c6ceafb..879b5fe36f 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -853,11 +853,9 @@ class PlaybackManager { user: user }); - for (let i = 0; i < responses.length; i++) { - const subTargets = responses[i]; - - for (let j = 0; j < subTargets.length; j++) { - targets.push(subTargets[j]); + for (const subTargets of responses) { + for (const subTarget of subTargets) { + targets.push(subTarget); } } diff --git a/src/components/playback/playerSelectionMenu.js b/src/components/playback/playerSelectionMenu.js index 17dcb06819..18c6fe7cdc 100644 --- a/src/components/playback/playerSelectionMenu.js +++ b/src/components/playback/playerSelectionMenu.js @@ -4,7 +4,7 @@ import browser from '../../scripts/browser'; import loading from '../loading/loading'; import { playbackManager } from '../playback/playbackmanager'; import { pluginManager } from '../pluginManager'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import globalize from '../../scripts/globalize'; import { appHost } from '../apphost'; import { enable, isEnabled, supported } from '../../scripts/autocast'; diff --git a/src/components/playbackSettings/playbackSettings.js b/src/components/playbackSettings/playbackSettings.js index d0056450a2..0e3a798b65 100644 --- a/src/components/playbackSettings/playbackSettings.js +++ b/src/components/playbackSettings/playbackSettings.js @@ -1,4 +1,3 @@ -import browser from '../../scripts/browser'; import appSettings from '../../scripts/settings/appSettings'; import { appHost } from '../apphost'; import focusManager from '../focusManager'; @@ -137,15 +136,6 @@ function showHideQualityFields(context, user, apiClient) { }); } -function showOrHideEpisodesField(context) { - if (browser.tizen || browser.web0s) { - context.querySelector('.fldEpisodeAutoPlay').classList.add('hide'); - return; - } - - context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide'); -} - function loadForm(context, user, userSettings, apiClient) { const loggedInUserId = apiClient.getCurrentUserId(); const userId = user.Id; @@ -209,8 +199,6 @@ function loadForm(context, user, userSettings, apiClient) { fillSkipLengths(selectSkipBackLength); selectSkipBackLength.value = userSettings.skipBackLength(); - showOrHideEpisodesField(context); - loading.hide(); } diff --git a/src/components/playbackSettings/playbackSettings.template.html b/src/components/playbackSettings/playbackSettings.template.html index 82970d15c1..44dfe85e0b 100644 --- a/src/components/playbackSettings/playbackSettings.template.html +++ b/src/components/playbackSettings/playbackSettings.template.html @@ -96,7 +96,7 @@
${CinemaModeConfigurationHelp}
-
+
= ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => { const element = useRef(null); diff --git a/src/components/search/SearchSuggestions.tsx b/src/components/search/SearchSuggestions.tsx index d16cdca8e6..aefb831fe0 100644 --- a/src/components/search/SearchSuggestions.tsx +++ b/src/components/search/SearchSuggestions.tsx @@ -5,7 +5,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import escapeHtml from 'escape-html'; import React, { FunctionComponent, useEffect, useState } from 'react'; -import { appRouter } from '../appRouter'; +import { appRouter } from '../router/appRouter'; import { useApi } from '../../hooks/useApi'; import globalize from '../../scripts/globalize'; @@ -25,7 +25,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) => type SearchSuggestionsProps = { parentId?: string | null; -} +}; const SearchSuggestions: FunctionComponent = ({ parentId }: SearchSuggestionsProps) => { const [ suggestions, setSuggestions ] = useState([]); @@ -45,7 +45,11 @@ const SearchSuggestions: FunctionComponent = ({ parentId parentId: parentId || undefined, enableTotalRecordCount: false }) - .then(result => setSuggestions(result.data.Items || [])); + .then(result => setSuggestions(result.data.Items || [])) + .catch(err => { + console.error('[SearchSuggestions] failed to fetch search suggestions', err); + setSuggestions([]); + }); } }, [ api, parentId, user ]); diff --git a/src/components/shortcuts.js b/src/components/shortcuts.js index 7c430be567..4dfbce8c5d 100644 --- a/src/components/shortcuts.js +++ b/src/components/shortcuts.js @@ -5,7 +5,7 @@ import { playbackManager } from './playback/playbackmanager'; import inputManager from '../scripts/inputManager'; -import { appRouter } from './appRouter'; +import { appRouter } from './router/appRouter'; import globalize from '../scripts/globalize'; import dom from '../scripts/dom'; import recordingHelper from './recordingcreator/recordinghelper'; diff --git a/src/components/viewManager/ViewManagerPage.tsx b/src/components/viewManager/ViewManagerPage.tsx index 563603e531..98cf0dcb01 100644 --- a/src/components/viewManager/ViewManagerPage.tsx +++ b/src/components/viewManager/ViewManagerPage.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import globalize from '../../scripts/globalize'; +import type { RestoreViewFailResponse } from '../../types/viewManager'; import viewManager from './viewManager'; export interface ViewManagerPageProps { @@ -45,7 +46,7 @@ const ViewManagerPage: FunctionComponent = ({ }; viewManager.tryRestoreView(viewOptions) - .catch(async (result?: any) => { + .catch(async (result?: RestoreViewFailResponse) => { if (!result || !result.cancelled) { const [ controllerFactory, viewHtml ] = await Promise.all([ import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`), @@ -63,7 +64,10 @@ const ViewManagerPage: FunctionComponent = ({ }; loadPage(); - }, [ + }, + // location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same + // eslint-disable-next-line react-hooks/exhaustive-deps + [ controller, view, type, @@ -73,8 +77,6 @@ const ViewManagerPage: FunctionComponent = ({ transition, location.pathname, location.search - // location.state is NOT included as a dependency here since dialogs will update state while the current view - // stays the same ]); return <>; diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index 8413c42dee..f28064e09a 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -90,11 +90,12 @@ function load(page, devices) { let html = ''; html += devices.map(function (device) { let deviceHtml = ''; - deviceHtml += "
"; + deviceHtml += "
"; deviceHtml += '
'; deviceHtml += ' +
+ +
${TonemappingModeHelp}
+
${LabelTonemappingDesatHelp}
-
- -
${LabelTonemappingThresholdHelp}
-
${LabelTonemappingPeakHelp}
diff --git a/src/controllers/dashboard/encodingsettings.js b/src/controllers/dashboard/encodingsettings.js index 38ca954d0e..2ae00b3dec 100644 --- a/src/controllers/dashboard/encodingsettings.js +++ b/src/controllers/dashboard/encodingsettings.js @@ -32,9 +32,9 @@ function loadPage(page, config, systemInfo) { page.querySelector('#chkTonemapping').checked = config.EnableTonemapping; page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping; page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm; + page.querySelector('#selectTonemappingMode').value = config.TonemappingMode; page.querySelector('#selectTonemappingRange').value = config.TonemappingRange; page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat; - page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold; page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak; page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || ''; page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness; @@ -90,9 +90,9 @@ function onSubmit() { config.EnableTonemapping = form.querySelector('#chkTonemapping').checked; config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked; config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value; + config.TonemappingMode = form.querySelector('#selectTonemappingMode').value; config.TonemappingRange = form.querySelector('#selectTonemappingRange').value; config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value; - config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value; config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value; config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0'; config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value; diff --git a/src/controllers/dashboard/plugins/add/index.js b/src/controllers/dashboard/plugins/add/index.js index 150fe3e1fd..62ae70ab4f 100644 --- a/src/controllers/dashboard/plugins/add/index.js +++ b/src/controllers/dashboard/plugins/add/index.js @@ -28,8 +28,7 @@ function populateVersions(packageInfo, page, installedPlugin) { return b.timestamp < a.timestamp ? -1 : 1; }); - for (let i = 0; i < packageInfo.versions.length; i++) { - const version = packageInfo.versions[i]; + for (const version of packageInfo.versions) { html += ''; } diff --git a/src/controllers/dashboard/plugins/available/index.js b/src/controllers/dashboard/plugins/available/index.js index 4dc172450f..78368b2c14 100644 --- a/src/controllers/dashboard/plugins/available/index.js +++ b/src/controllers/dashboard/plugins/available/index.js @@ -66,8 +66,7 @@ function populateList(options) { let currentCategory = null; let html = ''; - for (let i = 0; i < availablePlugins.length; i++) { - const plugin = availablePlugins[i]; + for (const plugin of availablePlugins) { const category = plugin.categoryDisplayName; if (category != currentCategory) { if (currentCategory) { diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index c074331aa1..8ae447aabc 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -133,8 +133,7 @@ function updateTaskButton(elem, state) { export default function(view) { function updateTasks(tasks) { - for (let i = 0; i < tasks.length; i++) { - const task = tasks[i]; + for (const task of tasks) { view.querySelector('#taskProgress' + task.Id).innerHTML = getTaskProgressHtml(task); updateTaskButton(view.querySelector('#btnTask' + task.Id), task.State); } diff --git a/src/controllers/dashboard/users/useredit.html b/src/controllers/dashboard/users/useredit.html deleted file mode 100644 index cf7d727ff7..0000000000 --- a/src/controllers/dashboard/users/useredit.html +++ /dev/null @@ -1,198 +0,0 @@ -
- -
-
- -
-
-

- ${Help} -
-
- - -

- ${ButtonEditOtherUserPreferences} -

-
- - -
- -
- -
- -
${AuthProviderHelp}
-
- -
- -
${PasswordResetProviderHelp}
-
- -
- -
${AllowRemoteAccessHelp}
-
- - -
-

${HeaderFeatureAccess}

-
- - -
-
-
-

${HeaderPlayback}

-
- - - - - -
-
${OptionAllowMediaPlaybackTranscodingHelp}
-
-
-
-
- -
${LabelRemoteClientBitrateLimitHelp}
-
${LabelUserRemoteClientBitrateLimitHelp}
-
-
-
-
- -
${SyncPlayAccessHelp}
-
-
-
-

${HeaderAllowMediaDeletionFrom}

-
- -
-
-
-
-
-

${HeaderRemoteControl}

-
- - - -
-
${OptionAllowRemoteSharedDevicesHelp}
-
-

${Other}

-
- -
${OptionAllowContentDownloadHelp}
-
-
- -
${OptionDisableUserHelp}
-
-
- -
${OptionHideUserFromLoginHelp}
-
-
-
-
- -
${OptionLoginAttemptsBeforeLockout}
-
${OptionLoginAttemptsBeforeLockoutHelp}
-
-
-
-
-
- -
${OptionMaxActiveSessions}
-
${OptionMaxActiveSessionsHelp}
-
-
-
-
- - - -
-
-
-
-
diff --git a/src/controllers/dashboard/users/useredit.js b/src/controllers/dashboard/users/useredit.js deleted file mode 100644 index 98aa0dd40e..0000000000 --- a/src/controllers/dashboard/users/useredit.js +++ /dev/null @@ -1,196 +0,0 @@ -import 'jquery'; -import loading from '../../../components/loading/loading'; -import libraryMenu from '../../../scripts/libraryMenu'; -import globalize from '../../../scripts/globalize'; -import Dashboard from '../../../utils/dashboard'; -import toast from '../../../components/toast/toast'; -import { getParameterByName } from '../../../utils/url.ts'; - -function loadDeleteFolders(page, user, mediaFolders) { - ApiClient.getJSON(ApiClient.getUrl('Channels', { - SupportsMediaDeletion: true - })).then(function (channelsResult) { - let isChecked; - let checkedAttribute; - let html = ''; - - for (const folder of mediaFolders) { - isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1; - checkedAttribute = isChecked ? ' checked="checked"' : ''; - html += ''; - } - - for (const folder of channelsResult.Items) { - isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1; - checkedAttribute = isChecked ? ' checked="checked"' : ''; - html += ''; - } - - $('.deleteAccess', page).html(html).trigger('create'); - $('#chkEnableDeleteAllFolders', page).prop('checked', user.Policy.EnableContentDeletion); - }); -} - -function loadAuthProviders(page, user, providers) { - if (providers.length > 1) { - page.querySelector('.fldSelectLoginProvider').classList.remove('hide'); - } else { - page.querySelector('.fldSelectLoginProvider').classList.add('hide'); - } - - const currentProviderId = user.Policy.AuthenticationProviderId; - page.querySelector('.selectLoginProvider').innerHTML = providers.map(function (provider) { - const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : ''; - return ''; - }); -} - -function loadPasswordResetProviders(page, user, providers) { - if (providers.length > 1) { - page.querySelector('.fldSelectPasswordResetProvider').classList.remove('hide'); - } else { - page.querySelector('.fldSelectPasswordResetProvider').classList.add('hide'); - } - - const currentProviderId = user.Policy.PasswordResetProviderId; - page.querySelector('.selectPasswordResetProvider').innerHTML = providers.map(function (provider) { - const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : ''; - return ''; - }); -} - -function loadUser(page, user) { - ApiClient.getJSON(ApiClient.getUrl('Auth/Providers')).then(function (providers) { - loadAuthProviders(page, user, providers); - }); - ApiClient.getJSON(ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) { - loadPasswordResetProviders(page, user, providers); - }); - ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', { - IsHidden: false - })).then(function (folders) { - loadDeleteFolders(page, user, folders.Items); - }); - - if (user.Policy.IsDisabled) { - $('.disabledUserBanner', page).show(); - } else { - $('.disabledUserBanner', page).hide(); - } - - $('#txtUserName', page).prop('disabled', '').removeAttr('disabled'); - $('#fldConnectInfo', page).show(); - $('.lnkEditUserPreferences', page).attr('href', 'mypreferencesmenu.html?userId=' + user.Id); - libraryMenu.setTitle(user.Name); - page.querySelector('.username').innerHTML = user.Name; - $('#txtUserName', page).val(user.Name); - $('#chkIsAdmin', page).prop('checked', user.Policy.IsAdministrator); - $('#chkDisabled', page).prop('checked', user.Policy.IsDisabled); - $('#chkIsHidden', page).prop('checked', user.Policy.IsHidden); - $('#chkEnableCollectionManagement', page).prop('checked', user.Policy.chkEnableCollectionManagement); - $('#chkRemoteControlSharedDevices', page).prop('checked', user.Policy.EnableSharedDeviceControl); - $('#chkEnableRemoteControlOtherUsers', page).prop('checked', user.Policy.EnableRemoteControlOfOtherUsers); - $('#chkEnableDownloading', page).prop('checked', user.Policy.EnableContentDownloading); - $('#chkManageLiveTv', page).prop('checked', user.Policy.EnableLiveTvManagement); - $('#chkEnableLiveTvAccess', page).prop('checked', user.Policy.EnableLiveTvAccess); - $('#chkEnableMediaPlayback', page).prop('checked', user.Policy.EnableMediaPlayback); - $('#chkEnableAudioPlaybackTranscoding', page).prop('checked', user.Policy.EnableAudioPlaybackTranscoding); - $('#chkEnableVideoPlaybackTranscoding', page).prop('checked', user.Policy.EnableVideoPlaybackTranscoding); - $('#chkEnableVideoPlaybackRemuxing', page).prop('checked', user.Policy.EnablePlaybackRemuxing); - $('#chkForceRemoteSourceTranscoding', page).prop('checked', user.Policy.ForceRemoteSourceTranscoding); - $('#chkRemoteAccess', page).prop('checked', user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess); - $('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || ''); - $('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0'); - $('#txtMaxActiveSessions', page).val(user.Policy.MaxActiveSessions || '0'); - if (ApiClient.isMinServerVersion('10.6.0')) { - $('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess); - } - loading.hide(); -} - -function onSaveComplete() { - Dashboard.navigate('userprofiles.html'); - loading.hide(); - toast(globalize.translate('SettingsSaved')); -} - -function saveUser(user, page) { - user.Name = $('#txtUserName', page).val(); - user.Policy.IsAdministrator = $('#chkIsAdmin', page).is(':checked'); - user.Policy.IsHidden = $('#chkIsHidden', page).is(':checked'); - user.Policy.IsDisabled = $('#chkDisabled', page).is(':checked'); - user.Policy.EnableRemoteControlOfOtherUsers = $('#chkEnableRemoteControlOtherUsers', page).is(':checked'); - user.Policy.EnableLiveTvManagement = $('#chkManageLiveTv', page).is(':checked'); - user.Policy.EnableLiveTvAccess = $('#chkEnableLiveTvAccess', page).is(':checked'); - user.Policy.EnableSharedDeviceControl = $('#chkRemoteControlSharedDevices', page).is(':checked'); - user.Policy.EnableMediaPlayback = $('#chkEnableMediaPlayback', page).is(':checked'); - user.Policy.EnableAudioPlaybackTranscoding = $('#chkEnableAudioPlaybackTranscoding', page).is(':checked'); - user.Policy.EnableVideoPlaybackTranscoding = $('#chkEnableVideoPlaybackTranscoding', page).is(':checked'); - user.Policy.EnablePlaybackRemuxing = $('#chkEnableVideoPlaybackRemuxing', page).is(':checked'); - user.Policy.EnableCollectionManagement = $('#chkEnableCollectionManagement', page).is(':checked'); - user.Policy.ForceRemoteSourceTranscoding = $('#chkForceRemoteSourceTranscoding', page).is(':checked'); - user.Policy.EnableContentDownloading = $('#chkEnableDownloading', page).is(':checked'); - user.Policy.EnableRemoteAccess = $('#chkRemoteAccess', page).is(':checked'); - user.Policy.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat($('#txtRemoteClientBitrateLimit', page).val() || '0'), 10); - user.Policy.LoginAttemptsBeforeLockout = parseInt($('#txtLoginAttemptsBeforeLockout', page).val() || '0', 10); - user.Policy.MaxActiveSessions = parseInt($('#txtMaxActiveSessions', page).val() || '0', 10); - user.Policy.AuthenticationProviderId = page.querySelector('.selectLoginProvider').value; - user.Policy.PasswordResetProviderId = page.querySelector('.selectPasswordResetProvider').value; - user.Policy.EnableContentDeletion = $('#chkEnableDeleteAllFolders', page).is(':checked'); - user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : $('.chkFolder', page).get().filter(function (c) { - return c.checked; - }).map(function (c) { - return c.getAttribute('data-id'); - }); - if (ApiClient.isMinServerVersion('10.6.0')) { - user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value; - } - ApiClient.updateUser(user).then(function () { - ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - onSaveComplete(); - }); - }); -} - -function onSubmit() { - const page = $(this).parents('.page')[0]; - loading.show(); - getUser().then(function (result) { - saveUser(result, page); - }); - return false; -} - -function getUser() { - const userId = getParameterByName('userId'); - return ApiClient.getUser(userId); -} - -function loadData(page) { - loading.show(); - getUser().then(function (user) { - loadUser(page, user); - }); -} - -$(document).on('pageinit', '#editUserPage', function () { - $('.editUserProfileForm').off('submit', onSubmit).on('submit', onSubmit); - const page = this; - $('#chkEnableDeleteAllFolders', this).on('change', function () { - if (this.checked) { - $('.deleteAccess', page).hide(); - } else { - $('.deleteAccess', page).show(); - } - }); - ApiClient.getServerConfiguration().then(function (config) { - if (config.EnableRemoteAccess) { - page.querySelector('.fldRemoteAccess').classList.remove('hide'); - } else { - page.querySelector('.fldRemoteAccess').classList.add('hide'); - } - }); -}).on('pagebeforeshow', '#editUserPage', function () { - loadData(this); -}); - diff --git a/src/controllers/dashboard/users/userlibraryaccess.html b/src/controllers/dashboard/users/userlibraryaccess.html deleted file mode 100644 index bf6ba93408..0000000000 --- a/src/controllers/dashboard/users/userlibraryaccess.html +++ /dev/null @@ -1,68 +0,0 @@ -
- -
-
- -
-
-

- ${Help} -
-
- - -
- -
-

${HeaderLibraryAccess}

- -
-
-
-
${LibraryAccessHelp}
-
-
- -
-
-

${HeaderDeviceAccess}

- -
-
-
-
${DeviceAccessHelp}
-
-
-
-
-
- -
-
-
-
-
diff --git a/src/controllers/dashboard/users/userlibraryaccess.js b/src/controllers/dashboard/users/userlibraryaccess.js deleted file mode 100644 index e84638e8e0..0000000000 --- a/src/controllers/dashboard/users/userlibraryaccess.js +++ /dev/null @@ -1,184 +0,0 @@ -import 'jquery'; -import loading from '../../../components/loading/loading'; -import libraryMenu from '../../../scripts/libraryMenu'; -import globalize from '../../../scripts/globalize'; -import Dashboard from '../../../utils/dashboard'; -import toast from '../../../components/toast/toast'; -import { getParameterByName } from '../../../utils/url.ts'; - -function triggerChange(select) { - const evt = document.createEvent('HTMLEvents'); - evt.initEvent('change', false, true); - select.dispatchEvent(evt); -} - -function loadMediaFolders(page, user, mediaFolders) { - let html = ''; - html += '

' + globalize.translate('HeaderLibraries') + '

'; - html += '
'; - - for (let i = 0, length = mediaFolders.length; i < length; i++) { - const folder = mediaFolders[i]; - const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1; - const checkedAttribute = isChecked ? ' checked="checked"' : ''; - html += ''; - } - - html += '
'; - page.querySelector('.folderAccess').innerHTML = html; - const chkEnableAllFolders = page.querySelector('#chkEnableAllFolders'); - chkEnableAllFolders.checked = user.Policy.EnableAllFolders; - triggerChange(chkEnableAllFolders); -} - -function loadChannels(page, user, channels) { - let html = ''; - html += '

' + globalize.translate('Channels') + '

'; - html += '
'; - - for (let i = 0, length = channels.length; i < length; i++) { - const folder = channels[i]; - const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1; - const checkedAttribute = isChecked ? ' checked="checked"' : ''; - html += ''; - } - - html += '
'; - $('.channelAccess', page).show().html(html); - - if (channels.length) { - $('.channelAccessContainer', page).show(); - } else { - $('.channelAccessContainer', page).hide(); - } - - const chkEnableAllChannels = page.querySelector('#chkEnableAllChannels'); - chkEnableAllChannels.checked = user.Policy.EnableAllChannels; - triggerChange(chkEnableAllChannels); -} - -function loadDevices(page, user, devices) { - let html = ''; - html += '

' + globalize.translate('HeaderDevices') + '

'; - html += '
'; - - for (let i = 0, length = devices.length; i < length; i++) { - const device = devices[i]; - const checkedAttribute = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1 ? ' checked="checked"' : ''; - html += ''; - } - - html += '
'; - $('.deviceAccess', page).show().html(html); - const chkEnableAllDevices = page.querySelector('#chkEnableAllDevices'); - chkEnableAllDevices.checked = user.Policy.EnableAllDevices; - triggerChange(chkEnableAllDevices); - - if (user.Policy.IsAdministrator) { - page.querySelector('.deviceAccessContainer').classList.add('hide'); - } else { - page.querySelector('.deviceAccessContainer').classList.remove('hide'); - } -} - -function loadUser(page, user, loggedInUser, mediaFolders, channels, devices) { - page.querySelector('.username').innerHTML = user.Name; - libraryMenu.setTitle(user.Name); - loadChannels(page, user, channels); - loadMediaFolders(page, user, mediaFolders); - loadDevices(page, user, devices); - loading.hide(); -} - -function onSaveComplete() { - loading.hide(); - toast(globalize.translate('SettingsSaved')); -} - -function saveUser(user, page) { - user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked'); - user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : $('.chkFolder', page).get().filter(function (c) { - return c.checked; - }).map(function (c) { - return c.getAttribute('data-id'); - }); - user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked'); - user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : $('.chkChannel', page).get().filter(function (c) { - return c.checked; - }).map(function (c) { - return c.getAttribute('data-id'); - }); - user.Policy.EnableAllDevices = $('#chkEnableAllDevices', page).is(':checked'); - user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : $('.chkDevice', page).get().filter(function (c) { - return c.checked; - }).map(function (c) { - return c.getAttribute('data-id'); - }); - user.Policy.BlockedChannels = null; - user.Policy.BlockedMediaFolders = null; - ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - onSaveComplete(); - }); -} - -function onSubmit() { - const page = $(this).parents('.page'); - loading.show(); - const userId = getParameterByName('userId'); - ApiClient.getUser(userId).then(function (result) { - saveUser(result, page); - }); - return false; -} - -$(document).on('pageinit', '#userLibraryAccessPage', function () { - const page = this; - $('#chkEnableAllDevices', page).on('change', function () { - if (this.checked) { - $('.deviceAccessListContainer', page).hide(); - } else { - $('.deviceAccessListContainer', page).show(); - } - }); - $('#chkEnableAllChannels', page).on('change', function () { - if (this.checked) { - $('.channelAccessListContainer', page).hide(); - } else { - $('.channelAccessListContainer', page).show(); - } - }); - page.querySelector('#chkEnableAllFolders').addEventListener('change', function () { - if (this.checked) { - page.querySelector('.folderAccessListContainer').classList.add('hide'); - } else { - page.querySelector('.folderAccessListContainer').classList.remove('hide'); - } - }); - $('.userLibraryAccessForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#userLibraryAccessPage', function () { - const page = this; - loading.show(); - let promise1; - const userId = getParameterByName('userId'); - - if (userId) { - promise1 = ApiClient.getUser(userId); - } else { - const deferred = $.Deferred(); - deferred.resolveWith(null, [{ - Configuration: {} - }]); - promise1 = deferred.promise(); - } - - const promise2 = Dashboard.getCurrentUser(); - const promise4 = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', { - IsHidden: false - })); - const promise5 = ApiClient.getJSON(ApiClient.getUrl('Channels')); - const promise6 = ApiClient.getJSON(ApiClient.getUrl('Devices')); - Promise.all([promise1, promise2, promise4, promise5, promise6]).then(function (responses) { - loadUser(page, responses[0], responses[1], responses[2].Items, responses[3].Items, responses[4].Items); - }); -}); - diff --git a/src/controllers/dashboard/users/usernew.html b/src/controllers/dashboard/users/usernew.html deleted file mode 100644 index 5d50ede80a..0000000000 --- a/src/controllers/dashboard/users/usernew.html +++ /dev/null @@ -1,62 +0,0 @@ -
-
-
-
-
-
-

${ButtonAddUser}

- ${Help} -
- -
- -
- -
- -
-
- -
-

${HeaderLibraryAccess}

-
- -
${LibraryAccessHelp}
-
-
-
-
-
-
- - - -
- - - -
-
-
-
-
diff --git a/src/controllers/dashboard/users/usernew.js b/src/controllers/dashboard/users/usernew.js deleted file mode 100644 index 9477506aca..0000000000 --- a/src/controllers/dashboard/users/usernew.js +++ /dev/null @@ -1,128 +0,0 @@ -import 'jquery'; -import loading from '../../../components/loading/loading'; -import globalize from '../../../scripts/globalize'; -import '../../../elements/emby-checkbox/emby-checkbox'; -import Dashboard from '../../../utils/dashboard'; -import toast from '../../../components/toast/toast'; - -function loadMediaFolders(page, mediaFolders) { - let html = ''; - html += '

' + globalize.translate('HeaderLibraries') + '

'; - html += '
'; - - for (let i = 0; i < mediaFolders.length; i++) { - const folder = mediaFolders[i]; - html += ''; - } - - html += '
'; - $('.folderAccess', page).html(html).trigger('create'); - $('#chkEnableAllFolders', page).prop('checked', false); -} - -function loadChannels(page, channels) { - let html = ''; - html += '

' + globalize.translate('Channels') + '

'; - html += '
'; - - for (let i = 0; i < channels.length; i++) { - const folder = channels[i]; - html += ''; - } - - html += '
'; - $('.channelAccess', page).show().html(html).trigger('create'); - - if (channels.length) { - $('.channelAccessContainer', page).show(); - } else { - $('.channelAccessContainer', page).hide(); - } - - $('#chkEnableAllChannels', page).prop('checked', false); -} - -function loadUser(page) { - $('#txtUsername', page).val(''); - $('#txtPassword', page).val(''); - loading.show(); - const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', { - IsHidden: false - })); - const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels')); - Promise.all([promiseFolders, promiseChannels]).then(function (responses) { - loadMediaFolders(page, responses[0].Items); - loadChannels(page, responses[1].Items); - loading.hide(); - }); -} - -function saveUser(page) { - const _user = { - Name: $('#txtUsername', page).val(), - Password: $('#txtPassword', page).val() - }; - ApiClient.createUser(_user).then(function (user) { - user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked'); - user.Policy.EnabledFolders = []; - - if (!user.Policy.EnableAllFolders) { - user.Policy.EnabledFolders = $('.chkFolder', page).get().filter(function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-id'); - }); - } - - user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked'); - user.Policy.EnabledChannels = []; - - if (!user.Policy.EnableAllChannels) { - user.Policy.EnabledChannels = $('.chkChannel', page).get().filter(function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-id'); - }); - } - - ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - Dashboard.navigate('useredit.html?userId=' + user.Id); - }); - }, function () { - toast(globalize.translate('ErrorDefault')); - loading.hide(); - }); -} - -function onSubmit() { - const page = $(this).parents('.page')[0]; - loading.show(); - saveUser(page); - return false; -} - -function loadData(page) { - loadUser(page); -} - -$(document).on('pageinit', '#newUserPage', function () { - const page = this; - $('#chkEnableAllChannels', page).on('change', function () { - if (this.checked) { - $('.channelAccessListContainer', page).hide(); - } else { - $('.channelAccessListContainer', page).show(); - } - }); - $('#chkEnableAllFolders', page).on('change', function () { - if (this.checked) { - $('.folderAccessListContainer', page).hide(); - } else { - $('.folderAccessListContainer', page).show(); - } - }); - $('.newUserProfileForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#newUserPage', function () { - loadData(this); -}); - diff --git a/src/controllers/dashboard/users/userparentalcontrol.html b/src/controllers/dashboard/users/userparentalcontrol.html deleted file mode 100644 index 5b58047c60..0000000000 --- a/src/controllers/dashboard/users/userparentalcontrol.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
-
-
-
-

- ${Help} -
-
- - - -
-
- -
${MaxParentalRatingHelp}
-
- -
-
-
- -
- -
-
-

${LabelBlockContentWithTags}

- -
-
-
- -
-
-

${HeaderAccessSchedule}

- -
- -

${HeaderAccessScheduleHelp}

-
-
- -
- -
-
-
-
-
diff --git a/src/controllers/dashboard/users/userparentalcontrol.js b/src/controllers/dashboard/users/userparentalcontrol.js deleted file mode 100644 index 0b527e09ef..0000000000 --- a/src/controllers/dashboard/users/userparentalcontrol.js +++ /dev/null @@ -1,278 +0,0 @@ -import 'jquery'; -import datetime from '../../../scripts/datetime'; -import loading from '../../../components/loading/loading'; -import libraryMenu from '../../../scripts/libraryMenu'; -import globalize from '../../../scripts/globalize'; -import '../../../components/listview/listview.scss'; -import '../../../elements/emby-button/paper-icon-button-light'; -import toast from '../../../components/toast/toast'; -import { getParameterByName } from '../../../utils/url.ts'; - -function populateRatings(allParentalRatings, page) { - let html = ''; - html += ""; - let rating; - const ratings = []; - - for (let i = 0, length = allParentalRatings.length; i < length; i++) { - rating = allParentalRatings[i]; - if (ratings.length) { - const lastRating = ratings[ratings.length - 1]; - - if (lastRating.Value === rating.Value) { - lastRating.Name += '/' + rating.Name; - continue; - } - } - - ratings.push({ - Name: rating.Name, - Value: rating.Value - }); - } - - for (let i = 0, length = ratings.length; i < length; i++) { - rating = ratings[i]; - html += "'; - } - - $('#selectMaxParentalRating', page).html(html); -} - -function loadUnratedItems(page, user) { - const items = [{ - name: globalize.translate('Books'), - value: 'Book' - }, { - name: globalize.translate('Channels'), - value: 'ChannelContent' - }, { - name: globalize.translate('LiveTV'), - value: 'LiveTvChannel' - }, { - name: globalize.translate('Movies'), - value: 'Movie' - }, { - name: globalize.translate('Music'), - value: 'Music' - }, { - name: globalize.translate('Trailers'), - value: 'Trailer' - }, { - name: globalize.translate('Shows'), - value: 'Series' - }]; - let html = ''; - html += '

' + globalize.translate('HeaderBlockItemsWithNoRating') + '

'; - html += '
'; - - for (let i = 0, length = items.length; i < length; i++) { - const item = items[i]; - const checkedAttribute = user.Policy.BlockUnratedItems.indexOf(item.value) != -1 ? ' checked="checked"' : ''; - html += ''; - } - - html += '
'; - $('.blockUnratedItems', page).html(html).trigger('create'); -} - -function loadUser(page, user, allParentalRatings) { - page.querySelector('.username').innerHTML = user.Name; - libraryMenu.setTitle(user.Name); - loadUnratedItems(page, user); - loadBlockedTags(page, user.Policy.BlockedTags); - populateRatings(allParentalRatings, page); - let ratingValue = ''; - - if (user.Policy.MaxParentalRating) { - for (let i = 0, length = allParentalRatings.length; i < length; i++) { - const rating = allParentalRatings[i]; - - if (user.Policy.MaxParentalRating >= rating.Value) { - ratingValue = rating.Value; - } - } - } - - $('#selectMaxParentalRating', page).val(ratingValue); - - if (user.Policy.IsAdministrator) { - $('.accessScheduleSection', page).hide(); - } else { - $('.accessScheduleSection', page).show(); - } - - renderAccessSchedule(page, user.Policy.AccessSchedules || []); - loading.hide(); -} - -function loadBlockedTags(page, tags) { - let html = tags.map(function (h) { - let li = '
'; - li += '
'; - li += '

'; - li += h; - li += '

'; - li += '
'; - li += ''; - li += '
'; - return li; - }).join(''); - - if (html) { - html = '
' + html + '
'; - } - - const blockedTags = page.querySelector('.blockedTags'); - blockedTags.innerHTML = html; - - for (const btnDeleteTag of blockedTags.querySelectorAll('.btnDeleteTag')) { - btnDeleteTag.addEventListener('click', function () { - const tag = this.getAttribute('data-tag'); - const newTags = tags.filter(function (t) { - return t != tag; - }); - loadBlockedTags(page, newTags); - }); - } -} - -function deleteAccessSchedule(page, schedules, index) { - schedules.splice(index, 1); - renderAccessSchedule(page, schedules); -} - -function renderAccessSchedule(page, schedules) { - let html = ''; - let index = 0; - html += schedules.map(function (a) { - let itemHtml = ''; - itemHtml += '
'; - itemHtml += '
'; - itemHtml += '

'; - itemHtml += globalize.translate('Option' + a.DayOfWeek); - itemHtml += '

'; - itemHtml += '
' + getDisplayTime(a.StartHour) + ' - ' + getDisplayTime(a.EndHour) + '
'; - itemHtml += '
'; - itemHtml += ''; - itemHtml += '
'; - index++; - return itemHtml; - }).join(''); - const accessScheduleList = page.querySelector('.accessScheduleList'); - accessScheduleList.innerHTML = html; - $('.btnDelete', accessScheduleList).on('click', function () { - deleteAccessSchedule(page, schedules, parseInt(this.getAttribute('data-index'), 10)); - }); -} - -function onSaveComplete() { - loading.hide(); - toast(globalize.translate('SettingsSaved')); -} - -function saveUser(user, page) { - user.Policy.MaxParentalRating = $('#selectMaxParentalRating', page).val() || null; - user.Policy.BlockUnratedItems = $('.chkUnratedItem', page).get().filter(function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-itemtype'); - }); - user.Policy.AccessSchedules = getSchedulesFromPage(page); - user.Policy.BlockedTags = getBlockedTagsFromPage(page); - ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - onSaveComplete(); - }); -} - -function getDisplayTime(hours) { - let minutes = 0; - const pct = hours % 1; - - if (pct) { - minutes = parseInt(60 * pct, 10); - } - - return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0)); -} - -function showSchedulePopup(page, schedule, index) { - schedule = schedule || {}; - import('../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { - accessschedule.show({ - schedule: schedule - }).then(function (updatedSchedule) { - const schedules = getSchedulesFromPage(page); - - if (index == -1) { - index = schedules.length; - } - - schedules[index] = updatedSchedule; - renderAccessSchedule(page, schedules); - }); - }); -} - -function getSchedulesFromPage(page) { - return $('.liSchedule', page).map(function () { - return { - DayOfWeek: this.getAttribute('data-day'), - StartHour: this.getAttribute('data-start'), - EndHour: this.getAttribute('data-end') - }; - }).get(); -} - -function getBlockedTagsFromPage(page) { - return $('.blockedTag', page).map(function () { - return this.getAttribute('data-tag'); - }).get(); -} - -function showBlockedTagPopup(page) { - import('../../../components/prompt/prompt').then(({ default: prompt }) => { - prompt({ - label: globalize.translate('LabelTag') - }).then(function (value) { - const tags = getBlockedTagsFromPage(page); - - if (tags.indexOf(value) == -1) { - tags.push(value); - loadBlockedTags(page, tags); - } - }); - }); -} - -window.UserParentalControlPage = { - onSubmit: function () { - const page = $(this).parents('.page'); - loading.show(); - const userId = getParameterByName('userId'); - ApiClient.getUser(userId).then(function (result) { - saveUser(result, page); - }); - return false; - } -}; -$(document).on('pageinit', '#userParentalControlPage', function () { - const page = this; - $('.btnAddSchedule', page).on('click', function () { - showSchedulePopup(page, {}, -1); - }); - $('.btnAddBlockedTag', page).on('click', function () { - showBlockedTagPopup(page); - }); - $('.userParentalControlForm').off('submit', UserParentalControlPage.onSubmit).on('submit', UserParentalControlPage.onSubmit); -}).on('pageshow', '#userParentalControlPage', function () { - const page = this; - loading.show(); - const userId = getParameterByName('userId'); - const promise1 = ApiClient.getUser(userId); - const promise2 = ApiClient.getParentalRatings(); - Promise.all([promise1, promise2]).then(function (responses) { - loadUser(page, responses[0], responses[1]); - }); -}); - diff --git a/src/controllers/dashboard/users/userpassword.html b/src/controllers/dashboard/users/userpassword.html deleted file mode 100644 index 897f0e7bd5..0000000000 --- a/src/controllers/dashboard/users/userpassword.html +++ /dev/null @@ -1,72 +0,0 @@ -
-
-
-
-
-

- ${Help} -
-
- - - -
-
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
-
- ${HeaderEasyPinCode} -
-
-
${EasyPasswordHelp}
-
-
- -
-
-
- -
${LabelInNetworkSignInWithEasyPasswordHelp}
-
-
- - -
-
-
-
-
-
-
diff --git a/src/controllers/dashboard/users/userpasswordpage.js b/src/controllers/dashboard/users/userpasswordpage.js deleted file mode 100644 index 4171c55d6e..0000000000 --- a/src/controllers/dashboard/users/userpasswordpage.js +++ /dev/null @@ -1,179 +0,0 @@ -import loading from '../../../components/loading/loading'; -import libraryMenu from '../../../scripts/libraryMenu'; -import globalize from '../../../scripts/globalize'; -import '../../../elements/emby-button/emby-button'; -import Dashboard from '../../../utils/dashboard'; -import toast from '../../../components/toast/toast'; -import confirm from '../../../components/confirm/confirm'; - -function loadUser(page, params) { - const userid = params.userId; - ApiClient.getUser(userid).then(function (user) { - Dashboard.getCurrentUser().then(function (loggedInUser) { - libraryMenu.setTitle(user.Name); - page.querySelector('.username').innerText = user.Name; - let showPasswordSection = true; - let showLocalAccessSection = false; - - if (user.ConnectLinkType == 'Guest') { - page.querySelector('.localAccessSection').classList.add('hide'); - showPasswordSection = false; - } else if (user.HasConfiguredPassword) { - page.querySelector('#btnResetPassword').classList.remove('hide'); - page.querySelector('#fldCurrentPassword').classList.remove('hide'); - showLocalAccessSection = true; - } else { - page.querySelector('#btnResetPassword').classList.add('hide'); - page.querySelector('#fldCurrentPassword').classList.add('hide'); - } - - if (showPasswordSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { - page.querySelector('.passwordSection').classList.remove('hide'); - } else { - page.querySelector('.passwordSection').classList.add('hide'); - } - - if (showLocalAccessSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { - page.querySelector('.localAccessSection').classList.remove('hide'); - } else { - page.querySelector('.localAccessSection').classList.add('hide'); - } - - const txtEasyPassword = page.querySelector('#txtEasyPassword'); - txtEasyPassword.value = ''; - - if (user.HasConfiguredEasyPassword) { - txtEasyPassword.placeholder = '******'; - page.querySelector('#btnResetEasyPassword').classList.remove('hide'); - } else { - txtEasyPassword.removeAttribute('placeholder'); - txtEasyPassword.placeholder = ''; - page.querySelector('#btnResetEasyPassword').classList.add('hide'); - } - - page.querySelector('.chkEnableLocalEasyPassword').checked = user.Configuration.EnableLocalPassword; - - import('../../../components/autoFocuser').then(({ default: autoFocuser }) => { - autoFocuser.autoFocus(page); - }); - }); - }); - page.querySelector('#txtCurrentPassword').value = ''; - page.querySelector('#txtNewPassword').value = ''; - page.querySelector('#txtNewPasswordConfirm').value = ''; -} - -export default function (view, params) { - function saveEasyPassword() { - const userId = params.userId; - const easyPassword = view.querySelector('#txtEasyPassword').value; - - if (easyPassword) { - ApiClient.updateEasyPassword(userId, easyPassword).then(function () { - onEasyPasswordSaved(userId); - }); - } else { - onEasyPasswordSaved(userId); - } - } - - function onEasyPasswordSaved(userId) { - ApiClient.getUser(userId).then(function (user) { - user.Configuration.EnableLocalPassword = view.querySelector('.chkEnableLocalEasyPassword').checked; - ApiClient.updateUserConfiguration(user.Id, user.Configuration).then(function () { - loading.hide(); - toast(globalize.translate('SettingsSaved')); - - loadUser(view, params); - }); - }); - } - - function savePassword() { - const userId = params.userId; - let currentPassword = view.querySelector('#txtCurrentPassword').value; - const newPassword = view.querySelector('#txtNewPassword').value; - - if (view.querySelector('#fldCurrentPassword').classList.contains('hide')) { - // Firefox does not respect autocomplete=off, so clear it if the field is supposed to be hidden (and blank) - // This should only happen when user.HasConfiguredPassword is false, but this information is not passed on - currentPassword = ''; - } - - ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () { - loading.hide(); - toast(globalize.translate('PasswordSaved')); - - loadUser(view, params); - }, function () { - loading.hide(); - Dashboard.alert({ - title: globalize.translate('HeaderLoginFailure'), - message: globalize.translate('MessageInvalidUser') - }); - }); - } - - function onSubmit(e) { - const form = this; - - if (form.querySelector('#txtNewPassword').value != form.querySelector('#txtNewPasswordConfirm').value) { - toast(globalize.translate('PasswordMatchError')); - } else { - loading.show(); - savePassword(); - } - - e.preventDefault(); - return false; - } - - function onLocalAccessSubmit(e) { - loading.show(); - saveEasyPassword(); - e.preventDefault(); - return false; - } - - function resetPassword() { - const msg = globalize.translate('PasswordResetConfirmation'); - confirm(msg, globalize.translate('ResetPassword')).then(function () { - const userId = params.userId; - loading.show(); - ApiClient.resetUserPassword(userId).then(function () { - loading.hide(); - Dashboard.alert({ - message: globalize.translate('PasswordResetComplete'), - title: globalize.translate('ResetPassword') - }); - loadUser(view, params); - }); - }); - } - - function resetEasyPassword() { - const msg = globalize.translate('PinCodeResetConfirmation'); - - confirm(msg, globalize.translate('HeaderPinCodeReset')).then(function () { - const userId = params.userId; - loading.show(); - ApiClient.resetEasyPassword(userId).then(function () { - loading.hide(); - Dashboard.alert({ - message: globalize.translate('PinCodeResetComplete'), - title: globalize.translate('HeaderPinCodeReset') - }); - loadUser(view, params); - }); - }); - } - - view.querySelector('.updatePasswordForm').addEventListener('submit', onSubmit); - view.querySelector('.localAccessForm').addEventListener('submit', onLocalAccessSubmit); - view.querySelector('#btnResetEasyPassword').addEventListener('click', resetEasyPassword); - view.querySelector('#btnResetPassword').addEventListener('click', resetPassword); - view.addEventListener('viewshow', function () { - loadUser(view, params); - }); -} - diff --git a/src/controllers/dashboard/users/userprofiles.html b/src/controllers/dashboard/users/userprofiles.html deleted file mode 100644 index 9e2908266b..0000000000 --- a/src/controllers/dashboard/users/userprofiles.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
-
-
-
-

${HeaderUsers}

- - ${Help} -
-
-
-
-
-
diff --git a/src/controllers/dashboard/users/userprofilespage.js b/src/controllers/dashboard/users/userprofilespage.js deleted file mode 100644 index 59d61a443f..0000000000 --- a/src/controllers/dashboard/users/userprofilespage.js +++ /dev/null @@ -1,184 +0,0 @@ -import loading from '../../../components/loading/loading'; -import dom from '../../../scripts/dom'; -import globalize from '../../../scripts/globalize'; -import { formatDistanceToNow } from 'date-fns'; -import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale.ts'; -import '../../../elements/emby-button/paper-icon-button-light'; -import '../../../components/cardbuilder/card.scss'; -import '../../../elements/emby-button/emby-button'; -import '../../../components/indicators/indicators.scss'; -import '../../../styles/flexstyles.scss'; -import Dashboard, { pageIdOn } from '../../../utils/dashboard'; -import confirm from '../../../components/confirm/confirm'; -import cardBuilder from '../../../components/cardbuilder/cardBuilder'; - -function deleteUser(page, id) { - const msg = globalize.translate('DeleteUserConfirmation'); - - confirm({ - title: globalize.translate('DeleteUser'), - text: msg, - confirmText: globalize.translate('Delete'), - primary: 'delete' - }).then(function () { - loading.show(); - ApiClient.deleteUser(id).then(function () { - loadData(page); - }); - }); -} - -function showUserMenu(elem) { - const card = dom.parentWithClass(elem, 'card'); - const page = dom.parentWithClass(card, 'page'); - const userId = card.getAttribute('data-userid'); - const menuItems = []; - menuItems.push({ - name: globalize.translate('ButtonOpen'), - id: 'open', - icon: 'mode_edit' - }); - menuItems.push({ - name: globalize.translate('ButtonLibraryAccess'), - id: 'access', - icon: 'lock' - }); - menuItems.push({ - name: globalize.translate('ButtonParentalControl'), - id: 'parentalcontrol', - icon: 'person' - }); - menuItems.push({ - name: globalize.translate('Delete'), - id: 'delete', - icon: 'delete' - }); - - import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => { - actionsheet.show({ - items: menuItems, - positionTo: card, - callback: function (id) { - switch (id) { - case 'open': - Dashboard.navigate('useredit.html?userId=' + userId); - break; - - case 'access': - Dashboard.navigate('userlibraryaccess.html?userId=' + userId); - break; - - case 'parentalcontrol': - Dashboard.navigate('userparentalcontrol.html?userId=' + userId); - break; - - case 'delete': - deleteUser(page, userId); - } - } - }); - }); -} - -function getUserHtml(user) { - let html = ''; - let cssClass = 'card squareCard scalableCard squareCard-scalable'; - - if (user.Policy.IsDisabled) { - cssClass += ' grayscale'; - } - - html += "
"; - html += '
'; - html += '
'; - html += '
'; - html += ``; - let imgUrl; - - if (user.PrimaryImageTag) { - imgUrl = ApiClient.getUserImageUrl(user.Id, { - width: 300, - tag: user.PrimaryImageTag, - type: 'Primary' - }); - } - - let imageClass = 'cardImage'; - - if (user.Policy.IsDisabled) { - imageClass += ' disabledUser'; - } - - if (imgUrl) { - html += ''; - html += '
'; - html += '
'; - html += '
'; - html += user.Name; - html += '
'; - html += ''; - html += '
'; - html += '
'; - const lastSeen = getLastSeenText(user.LastActivityDate); - html += lastSeen != '' ? lastSeen : ' '; - html += '
'; - html += '
'; - html += '
'; - return html + '
'; -} -// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix -// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences -function getLastSeenText(lastActivityDate) { - const localeWithSuffix = getLocaleWithSuffix(); - - if (lastActivityDate) { - return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix)); - } - - return ''; -} - -function getUserSectionHtml(users) { - return users.map(function (u__q) { - return getUserHtml(u__q); - }).join(''); -} - -function renderUsers(page, users) { - page.querySelector('.localUsers').innerHTML = getUserSectionHtml(users); -} - -function loadData(page) { - loading.show(); - ApiClient.getUsers().then(function (users) { - renderUsers(page, users); - loading.hide(); - }); -} - -pageIdOn('pageinit', 'userProfilesPage', function () { - const page = this; - page.querySelector('.btnAddUser').addEventListener('click', function() { - Dashboard.navigate('usernew.html'); - }); - page.querySelector('.localUsers').addEventListener('click', function (e__e) { - const btnUserMenu = dom.parentWithClass(e__e.target, 'btnUserMenu'); - - if (btnUserMenu) { - showUserMenu(btnUserMenu); - } - }); -}); - -pageIdOn('pagebeforeshow', 'userProfilesPage', function () { - loadData(this); -}); - diff --git a/src/controllers/favorites.js b/src/controllers/favorites.js index 97ce3eaf86..f14727a764 100644 --- a/src/controllers/favorites.js +++ b/src/controllers/favorites.js @@ -1,4 +1,4 @@ -import { appRouter } from '../components/appRouter'; +import { appRouter } from '../components/router/appRouter'; import cardBuilder from '../components/cardbuilder/cardBuilder'; import dom from '../scripts/dom'; import globalize from '../scripts/globalize'; diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 943b5f74a2..bb7aa47bea 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -6,7 +6,7 @@ import isEqual from 'lodash-es/isEqual'; import { appHost } from '../../components/apphost'; import loading from '../../components/loading/loading'; -import { appRouter } from '../../components/appRouter'; +import { appRouter } from '../../components/router/appRouter'; import layoutManager from '../../components/layoutManager'; import Events from '../../utils/events.ts'; import * as userSettings from '../../scripts/settings/userSettings'; diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js index ee0b04837c..52cb97bb79 100644 --- a/src/controllers/movies/moviegenres.js +++ b/src/controllers/movies/moviegenres.js @@ -5,7 +5,7 @@ import libraryBrowser from '../../scripts/libraryBrowser'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/appRouter'; +import { appRouter } from '../../components/router/appRouter'; import '../../elements/emby-button/emby-button'; export default function (view, params, tabContent) { diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 745170f5fc..c4c662cb9c 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -21,7 +21,7 @@ import '../../../styles/videoosd.scss'; import ServerConnections from '../../../components/ServerConnections'; import shell from '../../../scripts/shell'; import SubtitleSync from '../../../components/subtitlesync/subtitlesync'; -import { appRouter } from '../../../components/appRouter'; +import { appRouter } from '../../../components/router/appRouter'; import LibraryMenu from '../../../scripts/libraryMenu'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop'; import { pluginManager } from '../../../components/pluginManager'; @@ -539,11 +539,11 @@ export default function (view) { } function onBeginFetch() { - document.querySelector('.osdMediaStatus').classList.remove('hide'); + view.querySelector('.osdMediaStatus').classList.remove('hide'); } function onEndFetch() { - document.querySelector('.osdMediaStatus').classList.add('hide'); + view.querySelector('.osdMediaStatus').classList.add('hide'); } function bindToPlayer(player) { @@ -1437,7 +1437,7 @@ export default function (view) { const btnFastForward = view.querySelector('.btnFastForward'); const transitionEndEventName = dom.whichTransitionEvent(); const headerElement = document.querySelector('.skinHeader'); - const osdBottomElement = document.querySelector('.videoOsdBottom-maincontrols'); + const osdBottomElement = view.querySelector('.videoOsdBottom-maincontrols'); nowPlayingPositionSlider.enableKeyboardDragging(); nowPlayingVolumeSlider.enableKeyboardDragging(); diff --git a/src/controllers/session/addServer/index.js b/src/controllers/session/addServer/index.js index 548e6a388d..be339ca978 100644 --- a/src/controllers/session/addServer/index.js +++ b/src/controllers/session/addServer/index.js @@ -63,7 +63,7 @@ export default function(view) { } function goBack() { - import('../../../components/appRouter').then(({ appRouter }) => { + import('../../../components/router/appRouter').then(({ appRouter }) => { appRouter.back(); }); } diff --git a/src/controllers/session/login/index.js b/src/controllers/session/login/index.js index 0f2d18580c..6e75af6f08 100644 --- a/src/controllers/session/login/index.js +++ b/src/controllers/session/login/index.js @@ -135,9 +135,7 @@ function showManualForm(context, showCancel, focusPassword) { function loadUserList(context, apiClient, users) { let html = ''; - for (let i = 0; i < users.length; i++) { - const user = users[i]; - + for (const user of users) { // TODO move card creation code to Card component let cssClass = 'card squareCard scalableCard squareCard-scalable'; diff --git a/src/controllers/session/selectServer/index.js b/src/controllers/session/selectServer/index.js index 8d3edbf3cb..fe14164ddd 100644 --- a/src/controllers/session/selectServer/index.js +++ b/src/controllers/session/selectServer/index.js @@ -1,6 +1,6 @@ import escapeHtml from 'escape-html'; import loading from '../../../components/loading/loading'; -import { appRouter } from '../../../components/appRouter'; +import { appRouter } from '../../../components/router/appRouter'; import layoutManager from '../../../components/layoutManager'; import libraryMenu from '../../../scripts/libraryMenu'; import appSettings from '../../../scripts/settings/appSettings'; diff --git a/src/controllers/shows/tvgenres.js b/src/controllers/shows/tvgenres.js index bc27ad4759..b9e02039eb 100644 --- a/src/controllers/shows/tvgenres.js +++ b/src/controllers/shows/tvgenres.js @@ -5,7 +5,7 @@ import libraryBrowser from '../../scripts/libraryBrowser'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/appRouter'; +import { appRouter } from '../../components/router/appRouter'; import '../../elements/emby-button/emby-button'; export default function (view, params, tabContent) { diff --git a/src/controllers/user/profile/index.html b/src/controllers/user/profile/index.html deleted file mode 100644 index 3eaa2f7299..0000000000 --- a/src/controllers/user/profile/index.html +++ /dev/null @@ -1,69 +0,0 @@ -
-
-
-
- -
-
-
-

-
- - -
-
-
-
-

- ${HeaderPassword} -

-
- -
-
- -
-
- -
-
- - -
-
-
-
-
-

${HeaderEasyPinCode}

-
${EasyPasswordHelp}
-
-
- -
-
- -
${LabelInNetworkSignInWithEasyPasswordHelp}
-
-
- - -
-
-
-
-
diff --git a/src/controllers/user/profile/index.js b/src/controllers/user/profile/index.js deleted file mode 100644 index 07bab611c3..0000000000 --- a/src/controllers/user/profile/index.js +++ /dev/null @@ -1,105 +0,0 @@ -import UserPasswordPage from '../../dashboard/users/userpasswordpage'; -import loading from '../../../components/loading/loading'; -import libraryMenu from '../../../scripts/libraryMenu'; -import { appHost } from '../../../components/apphost'; -import globalize from '../../../scripts/globalize'; -import '../../../elements/emby-button/emby-button'; -import Dashboard from '../../../utils/dashboard'; -import toast from '../../../components/toast/toast'; -import confirm from '../../../components/confirm/confirm'; -import { getParameterByName } from '../../../utils/url.ts'; - -function reloadUser(page) { - const userId = getParameterByName('userId'); - loading.show(); - ApiClient.getUser(userId).then(function (user) { - page.querySelector('.username').innerText = user.Name; - libraryMenu.setTitle(user.Name); - - let imageUrl = 'assets/img/avatar.png'; - if (user.PrimaryImageTag) { - imageUrl = ApiClient.getUserImageUrl(user.Id, { - tag: user.PrimaryImageTag, - type: 'Primary' - }); - } - - const userImage = page.querySelector('#image'); - userImage.style.backgroundImage = 'url(' + imageUrl + ')'; - - Dashboard.getCurrentUser().then(function (loggedInUser) { - if (user.PrimaryImageTag) { - page.querySelector('#btnAddImage').classList.add('hide'); - page.querySelector('#btnDeleteImage').classList.remove('hide'); - } else if (appHost.supports('fileinput') && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { - page.querySelector('#btnDeleteImage').classList.add('hide'); - page.querySelector('#btnAddImage').classList.remove('hide'); - } - }); - loading.hide(); - }); -} - -function onFileReaderError(evt) { - loading.hide(); - switch (evt.target.error.code) { - case evt.target.error.NOT_FOUND_ERR: - toast(globalize.translate('FileNotFound')); - break; - case evt.target.error.ABORT_ERR: - onFileReaderAbort(); - break; - case evt.target.error.NOT_READABLE_ERR: - default: - toast(globalize.translate('FileReadError')); - } -} - -function onFileReaderAbort() { - loading.hide(); - toast(globalize.translate('FileReadCancelled')); -} - -function setFiles(page, files) { - const userImage = page.querySelector('#image'); - const file = files[0]; - - if (!file || !file.type.match('image.*')) { - return false; - } - - const reader = new FileReader(); - reader.onerror = onFileReaderError; - reader.onabort = onFileReaderAbort; - reader.onload = function (evt) { - userImage.style.backgroundImage = 'url(' + evt.target.result + ')'; - const userId = getParameterByName('userId'); - ApiClient.uploadUserImage(userId, 'Primary', file).then(function () { - loading.hide(); - reloadUser(page); - }); - }; - - reader.readAsDataURL(file); -} - -export default function (view, params) { - reloadUser(view); - new UserPasswordPage(view, params); - view.querySelector('#btnDeleteImage').addEventListener('click', function () { - confirm(globalize.translate('DeleteImageConfirmation'), globalize.translate('DeleteImage')).then(function () { - loading.show(); - const userId = getParameterByName('userId'); - ApiClient.deleteUserImage(userId, 'primary').then(function () { - loading.hide(); - reloadUser(view); - }); - }); - }); - view.querySelector('#btnAddImage').addEventListener('click', function () { - view.querySelector('#uploadImage').click(); - }); - view.querySelector('#uploadImage').addEventListener('change', function (evt) { - setFiles(view, evt.target.files); - }); -} diff --git a/src/elements/ButtonElement.tsx b/src/elements/ButtonElement.tsx index 3de90f0c11..f9ed58954b 100644 --- a/src/elements/ButtonElement.tsx +++ b/src/elements/ButtonElement.tsx @@ -21,7 +21,7 @@ type IProps = { title?: string; leftIcon?: string; rightIcon?: string; -} +}; const ButtonElement: FunctionComponent = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => { return ( diff --git a/src/elements/CheckBoxElement.tsx b/src/elements/CheckBoxElement.tsx index fade823891..c8d861a0bb 100644 --- a/src/elements/CheckBoxElement.tsx +++ b/src/elements/CheckBoxElement.tsx @@ -29,7 +29,7 @@ type IProps = { itemCheckedAttribute?: string; itemName?: string title?: string -} +}; const CheckBoxElement: FunctionComponent = ({ labelClassName, className, elementId, dataFilter, itemType, itemId, itemAppName, itemCheckedAttribute, itemName, title }: IProps) => { const appName = itemAppName ? `- ${itemAppName}` : ''; diff --git a/src/elements/IconButtonElement.tsx b/src/elements/IconButtonElement.tsx index b39847403b..50b4d76f6e 100644 --- a/src/elements/IconButtonElement.tsx +++ b/src/elements/IconButtonElement.tsx @@ -10,7 +10,7 @@ type IProps = { dataIndex?: string | number; dataTag?: string | number; dataProfileid?: string | number; -} +}; const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({ __html: `