1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'jellyfin:master' into audio-normalization

This commit is contained in:
TelepathicWalrus 2023-05-07 21:34:40 +01:00 committed by GitHub
commit 91210408f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
203 changed files with 2214 additions and 3733 deletions

View file

@ -2,8 +2,9 @@ const restrictedGlobals = require('confusing-browser-globals');
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser',
plugins: [ plugins: [
'@babel', '@typescript-eslint',
'react', 'react',
'promise', 'promise',
'import', 'import',
@ -16,14 +17,6 @@ module.exports = {
es2017: true, es2017: true,
es2020: true es2020: true
}, },
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
jsx: true
}
},
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:react/recommended', 'plugin:react/recommended',
@ -53,14 +46,19 @@ module.exports = {
'no-multi-spaces': ['error'], 'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }], 'no-multiple-empty-lines': ['error', { 'max': 1 }],
'no-nested-ternary': ['error'], 'no-nested-ternary': ['error'],
'no-redeclare': ['off'],
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
'no-restricted-globals': ['error'].concat(restrictedGlobals), 'no-restricted-globals': ['error'].concat(restrictedGlobals),
'no-return-assign': ['error'], 'no-return-assign': ['error'],
'no-return-await': ['error'], 'no-return-await': ['error'],
'no-sequences': ['error', { 'allowInParentheses': false }], 'no-sequences': ['error', { 'allowInParentheses': false }],
'no-shadow': ['error'], 'no-shadow': ['off'],
'@typescript-eslint/no-shadow': ['error'],
'no-trailing-spaces': ['error'], 'no-trailing-spaces': ['error'],
'@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], 'no-unused-expressions': ['off'],
'no-useless-constructor': ['error'], '@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-var': ['error'],
'no-void': ['error', { 'allowAsStatement': true }], 'no-void': ['error', { 'allowAsStatement': true }],
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }], 'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
@ -69,9 +67,10 @@ module.exports = {
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }], 'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'], 'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }], 'prefer-const': ['error', { 'destructuring': 'all' }],
'@typescript-eslint/prefer-for-of': ['error'],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'radix': ['error'], 'radix': ['error'],
'@babel/semi': ['error'], '@typescript-eslint/semi': ['error'],
'space-before-blocks': ['error'], 'space-before-blocks': ['error'],
'space-infix-ops': 'error', 'space-infix-ops': 'error',
'yoda': 'error', 'yoda': 'error',
@ -201,9 +200,12 @@ module.exports = {
files: [ files: [
'./src/**/*.js', './src/**/*.js',
'./src/**/*.jsx', './src/**/*.jsx',
'./src/**/*.ts' './src/**/*.ts',
'./src/**/*.tsx'
], ],
parser: '@babel/eslint-parser', parserOptions: {
project: ['./tsconfig.json']
},
env: { env: {
node: false, node: false,
amd: true, amd: true,
@ -243,6 +245,7 @@ module.exports = {
'Windows': 'readonly' 'Windows': 'readonly'
}, },
rules: { rules: {
'@typescript-eslint/no-floating-promises': ['warn']
} }
}, },
// TypeScript source files // TypeScript source files
@ -251,8 +254,6 @@ module.exports = {
'./src/**/*.ts', './src/**/*.ts',
'./src/**/*.tsx' './src/**/*.tsx'
], ],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:import/typescript', 'plugin:import/typescript',
@ -263,13 +264,9 @@ module.exports = {
'plugin:jsx-a11y/recommended' 'plugin:jsx-a11y/recommended'
], ],
rules: { rules: {
// Use TypeScript equivalent rules when required '@typescript-eslint/no-floating-promises': ['error'],
'no-shadow': ['off'],
'@typescript-eslint/no-shadow': ['error'],
'no-useless-constructor': ['off'],
'@typescript-eslint/no-useless-constructor': ['error'],
'sonarjs/cognitive-complexity': ['warn'] 'sonarjs/cognitive-complexity': ['error']
} }
} }
] ]

View file

@ -21,11 +21,11 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0 uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - 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: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
@ -28,7 +28,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Comment on failure - name: Comment on failure
if: 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: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}

View file

@ -76,21 +76,24 @@ Jellyfin Web is the frontend used for most of the clients available for end user
``` ```
. .
└── src └── src
├── assets # Static assets ├── apps
├── components # Higher order visual components and React components │   ├── experimental # New experimental app layout
├── controllers # Legacy page views and controllers 🧹 │   └── stable # Classic (stable) app layout
├── elements # Basic webcomponents and React wrappers 🧹 ├── assets # Static assets
├── hooks # Custom React hooks ├── components # Higher order visual components and React components
├── legacy # Polyfills for legacy browsers ├── controllers # Legacy page views and controllers 🧹
├── libraries # Third party libraries 🧹 ├── elements # Basic webcomponents and React wrappers 🧹
├── plugins # Client plugins ├── hooks # Custom React hooks
├── routes # React routes/pages ├── legacy # Polyfills for legacy browsers
├── scripts # Random assortment of visual components and utilities 🐉 ├── libraries # Third party libraries 🧹
├── strings # Translation files ├── plugins # Client plugins
├── styles # Common app Sass stylesheets ├── routes # React routes/pages
├── themes # CSS themes ├── scripts # Random assortment of visual components and utilities 🐉
├── types # Common TypeScript interfaces/types ├── strings # Translation files
└── utils # Utility functions ├── styles # Common app Sass stylesheets
├── themes # CSS themes
├── types # Common TypeScript interfaces/types
└── utils # Utility functions
``` ```
- 🧹 — Needs cleanup - 🧹 — Needs cleanup

View file

@ -1,4 +1,4 @@
FROM fedora:38 FROM fedora:39
# Docker build arguments # Docker build arguments
ARG SOURCE_DIR=/jellyfin ARG SOURCE_DIR=/jellyfin

523
package-lock.json generated
View file

@ -32,7 +32,7 @@
"history": "5.3.0", "history": "5.3.0",
"hls.js": "1.4.0", "hls.js": "1.4.0",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"jassub": "1.5.12", "jassub": "1.5.13",
"jellyfin-apiclient": "1.10.0", "jellyfin-apiclient": "1.10.0",
"jquery": "3.6.4", "jquery": "3.6.4",
"jstree": "3.3.15", "jstree": "3.3.15",
@ -48,7 +48,7 @@
"resize-observer-polyfill": "1.5.1", "resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"swiper": "8.4.7", "swiper": "9.2.4",
"webcomponents.js": "0.7.24", "webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.2", "whatwg-fetch": "3.6.2",
"workbox-core": "6.5.4", "workbox-core": "6.5.4",
@ -56,8 +56,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.4", "@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-class-properties": "7.18.6",
"@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-transform-modules-umd": "7.18.6", "@babel/plugin-transform-modules-umd": "7.18.6",
@ -67,9 +65,9 @@
"@types/loadable__component": "5.13.4", "@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.7", "@types/lodash-es": "4.17.7",
"@types/react": "17.0.58", "@types/react": "17.0.58",
"@types/react-dom": "17.0.19", "@types/react-dom": "17.0.20",
"@typescript-eslint/eslint-plugin": "5.58.0", "@typescript-eslint/eslint-plugin": "5.59.1",
"@typescript-eslint/parser": "5.58.0", "@typescript-eslint/parser": "5.59.1",
"@uupaa/dynamic-import-polyfill": "1.0.2", "@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
@ -81,7 +79,7 @@
"css-loader": "6.7.3", "css-loader": "6.7.3",
"cssnano": "6.0.0", "cssnano": "6.0.0",
"es-check": "7.1.1", "es-check": "7.1.1",
"eslint": "8.38.0", "eslint": "8.39.0",
"eslint-plugin-compat": "4.1.4", "eslint-plugin-compat": "4.1.4",
"eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
@ -92,17 +90,17 @@
"eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-sonarjs": "0.19.0",
"expose-loader": "4.1.0", "expose-loader": "4.1.0",
"html-loader": "4.2.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", "mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.21", "postcss": "8.4.23",
"postcss-loader": "7.2.4", "postcss-loader": "7.2.4",
"postcss-preset-env": "8.3.1", "postcss-preset-env": "8.3.2",
"postcss-scss": "4.0.6", "postcss-scss": "4.0.6",
"sass": "1.62.0", "sass": "1.62.1",
"sass-loader": "13.2.2", "sass-loader": "13.2.2",
"source-map-loader": "4.0.1", "source-map-loader": "4.0.1",
"style-loader": "3.3.2", "style-loader": "3.3.2",
"stylelint": "15.4.0", "stylelint": "15.6.0",
"stylelint-config-rational-order": "0.1.2", "stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1", "stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
@ -111,7 +109,7 @@
"typescript": "5.0.4", "typescript": "5.0.4",
"webpack": "5.79.0", "webpack": "5.79.0",
"webpack-cli": "5.0.1", "webpack-cli": "5.0.1",
"webpack-dev-server": "4.13.2", "webpack-dev-server": "4.13.3",
"webpack-merge": "5.8.0", "webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.4", "workbox-webpack-plugin": "6.5.4",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
@ -225,40 +223,6 @@
"url": "https://opencollective.com/babel" "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": { "node_modules/@babel/generator": {
"version": "7.21.4", "version": "7.21.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz",
@ -2667,9 +2631,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.38.0", "version": "8.39.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
"integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -2902,15 +2866,6 @@
"node": ">=4" "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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -3304,9 +3259,9 @@
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "17.0.19", "version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz",
"integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/react": "^17" "@types/react": "^17"
@ -3410,15 +3365,15 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz",
"integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/type-utils": "5.58.0", "@typescript-eslint/type-utils": "5.59.1",
"@typescript-eslint/utils": "5.58.0", "@typescript-eslint/utils": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"grapheme-splitter": "^1.0.4", "grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0", "ignore": "^5.2.0",
@ -3459,14 +3414,14 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz",
"integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -3486,13 +3441,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz",
"integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/visitor-keys": "5.58.0" "@typescript-eslint/visitor-keys": "5.59.1"
}, },
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -3503,13 +3458,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz",
"integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"@typescript-eslint/utils": "5.58.0", "@typescript-eslint/utils": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
}, },
@ -3530,9 +3485,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz",
"integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -3543,13 +3498,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz",
"integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/visitor-keys": "5.58.0", "@typescript-eslint/visitor-keys": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -3614,17 +3569,17 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz",
"integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12", "@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"eslint-scope": "^5.1.1", "eslint-scope": "^5.1.1",
"semver": "^7.3.7" "semver": "^7.3.7"
}, },
@ -3655,12 +3610,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz",
"integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"eslint-visitor-keys": "^3.3.0" "eslint-visitor-keys": "^3.3.0"
}, },
"engines": { "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": { "node_modules/domelementtype": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
@ -6798,15 +6745,15 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.38.0", "version": "8.39.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
"integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.2", "@eslint/eslintrc": "^2.0.2",
"@eslint/js": "8.38.0", "@eslint/js": "8.39.0",
"@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@ -6816,7 +6763,7 @@
"debug": "^4.3.2", "debug": "^4.3.2",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1", "eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.0", "eslint-visitor-keys": "^3.4.0",
"espree": "^9.5.1", "espree": "^9.5.1",
"esquery": "^1.4.2", "esquery": "^1.4.2",
@ -7240,15 +7187,6 @@
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@ -7262,15 +7200,6 @@
"node": ">=8.0.0" "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": { "node_modules/eslint/node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -9054,9 +8983,9 @@
} }
}, },
"node_modules/html-tags": { "node_modules/html-tags": {
"version": "3.2.0", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -9066,9 +8995,9 @@
} }
}, },
"node_modules/html-webpack-plugin": { "node_modules/html-webpack-plugin": {
"version": "5.5.0", "version": "5.5.1",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz",
"integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/html-minifier-terser": "^6.0.0", "@types/html-minifier-terser": "^6.0.0",
@ -10101,9 +10030,9 @@
} }
}, },
"node_modules/jassub": { "node_modules/jassub": {
"version": "1.5.12", "version": "1.5.13",
"resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.13.tgz",
"integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", "integrity": "sha512-mQM88BcYgppvpPG6VE+DPQm7r6QS65EBedbm13RE4lRIhdrnQ+ihWhBOZXYZe3SlGhg+ROIDRK8uY4dm9ER2XQ==",
"dependencies": { "dependencies": {
"rvfc-polyfill": "^1.0.4" "rvfc-polyfill": "^1.0.4"
} }
@ -11042,10 +10971,16 @@
"optional": true "optional": true
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.4", "version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true, "dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@ -11887,9 +11822,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.21", "version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -11899,10 +11834,14 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss" "url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
@ -13144,16 +13083,26 @@
} }
}, },
"node_modules/postcss-preset-env": { "node_modules/postcss-preset-env": {
"version": "8.3.1", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.1.tgz", "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.2.tgz",
"integrity": "sha512-k3Y8BXbVLBAufrla3CNmQJhMS1iRuT9LFlysYvzs1rU5E78+ShX2u0EUL6KpMi0pDJO3wZcuVYSR8cgukfoRtg==", "integrity": "sha512-VSAOsfxTXzO/gX5QljC8x8hN3ABbD9iqqLgqHqohBdNI5FhJptwpl96kpu+kYvvzK7BWwaHYou0IeYrp+NqmcQ==",
"dev": true, "dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"dependencies": { "dependencies": {
"@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-cascade-layers": "^3.0.1",
"@csstools/postcss-color-function": "^2.2.1", "@csstools/postcss-color-function": "^2.2.1",
"@csstools/postcss-color-mix-function": "^1.0.1", "@csstools/postcss-color-mix-function": "^1.0.1",
"@csstools/postcss-font-format-keywords": "^2.0.2", "@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-hwb-function": "^2.2.1",
"@csstools/postcss-ic-unit": "^2.0.2", "@csstools/postcss-ic-unit": "^2.0.2",
"@csstools/postcss-is-pseudo-class": "^3.2.0", "@csstools/postcss-is-pseudo-class": "^3.2.0",
@ -13208,10 +13157,6 @@
"engines": { "engines": {
"node": "^14 || ^16 || >=18" "node": "^14 || ^16 || >=18"
}, },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": { "peerDependencies": {
"postcss": "^8.4" "postcss": "^8.4"
} }
@ -14325,9 +14270,9 @@
"dev": true "dev": true
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.62.0", "version": "1.62.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
"integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
@ -15450,14 +15395,14 @@
} }
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "15.4.0", "version": "15.6.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.4.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.0.tgz",
"integrity": "sha512-TlOvpG3MbcFwHmK0q2ykhmpKo7Dq892beJit0NPdpyY9b1tFah/hGhqnAz/bRm2PDhDbJLKvjzkEYYBEz7Dxcg==", "integrity": "sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@csstools/css-parser-algorithms": "^2.1.0", "@csstools/css-parser-algorithms": "^2.1.1",
"@csstools/css-tokenizer": "^2.1.0", "@csstools/css-tokenizer": "^2.1.1",
"@csstools/media-query-list-parser": "^2.0.1", "@csstools/media-query-list-parser": "^2.0.4",
"@csstools/selector-specificity": "^2.2.0", "@csstools/selector-specificity": "^2.2.0",
"balanced-match": "^2.0.0", "balanced-match": "^2.0.0",
"colord": "^2.9.3", "colord": "^2.9.3",
@ -15471,7 +15416,7 @@
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^11.1.0", "globby": "^11.1.0",
"globjoin": "^0.1.4", "globjoin": "^0.1.4",
"html-tags": "^3.2.0", "html-tags": "^3.3.1",
"ignore": "^5.2.4", "ignore": "^5.2.4",
"import-lazy": "^4.0.0", "import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
@ -15482,7 +15427,7 @@
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"postcss": "^8.4.21", "postcss": "^8.4.22",
"postcss-media-query-parser": "^0.2.3", "postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1", "postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0", "postcss-safe-parser": "^6.0.0",
@ -18141,9 +18086,9 @@
} }
}, },
"node_modules/swiper": { "node_modules/swiper": {
"version": "8.4.7", "version": "9.2.4",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.2.4.tgz",
"integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", "integrity": "sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==",
"funding": [ "funding": [
{ {
"type": "patreon", "type": "patreon",
@ -18154,9 +18099,7 @@
"url": "http://opencollective.com/swiper" "url": "http://opencollective.com/swiper"
} }
], ],
"hasInstallScript": true,
"dependencies": { "dependencies": {
"dom7": "^4.0.4",
"ssr-window": "^4.0.2" "ssr-window": "^4.0.2"
}, },
"engines": { "engines": {
@ -19308,9 +19251,9 @@
"dev": true "dev": true
}, },
"node_modules/webpack-dev-server": { "node_modules/webpack-dev-server": {
"version": "4.13.2", "version": "4.13.3",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz",
"integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/bonjour": "^3.5.9", "@types/bonjour": "^3.5.9",
@ -20165,26 +20108,6 @@
"semver": "^6.3.0" "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": { "@babel/generator": {
"version": "7.21.4", "version": "7.21.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz",
@ -21748,9 +21671,9 @@
} }
}, },
"@eslint/js": { "@eslint/js": {
"version": "8.38.0", "version": "8.39.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
"integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
"dev": true "dev": true
}, },
"@fontsource/noto-sans": { "@fontsource/noto-sans": {
@ -21932,15 +21855,6 @@
"glob-to-regexp": "^0.3.0" "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": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -22292,9 +22206,9 @@
} }
}, },
"@types/react-dom": { "@types/react-dom": {
"version": "17.0.19", "version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz",
"integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/react": "^17" "@types/react": "^17"
@ -22397,15 +22311,15 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz",
"integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==", "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/type-utils": "5.58.0", "@typescript-eslint/type-utils": "5.59.1",
"@typescript-eslint/utils": "5.58.0", "@typescript-eslint/utils": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"grapheme-splitter": "^1.0.4", "grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0", "ignore": "^5.2.0",
@ -22426,53 +22340,53 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz",
"integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==", "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"debug": "^4.3.4" "debug": "^4.3.4"
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz",
"integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==", "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/visitor-keys": "5.58.0" "@typescript-eslint/visitor-keys": "5.59.1"
} }
}, },
"@typescript-eslint/type-utils": { "@typescript-eslint/type-utils": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz",
"integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==", "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"@typescript-eslint/utils": "5.58.0", "@typescript-eslint/utils": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"tsutils": "^3.21.0" "tsutils": "^3.21.0"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz",
"integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==", "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz",
"integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==", "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/visitor-keys": "5.58.0", "@typescript-eslint/visitor-keys": "5.59.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -22512,17 +22426,17 @@
} }
}, },
"@typescript-eslint/utils": { "@typescript-eslint/utils": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz",
"integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==", "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12", "@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.58.0", "@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"@typescript-eslint/typescript-estree": "5.58.0", "@typescript-eslint/typescript-estree": "5.59.1",
"eslint-scope": "^5.1.1", "eslint-scope": "^5.1.1",
"semver": "^7.3.7" "semver": "^7.3.7"
}, },
@ -22539,12 +22453,12 @@
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "5.58.0", "version": "5.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz",
"integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==", "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "5.58.0", "@typescript-eslint/types": "5.59.1",
"eslint-visitor-keys": "^3.3.0" "eslint-visitor-keys": "^3.3.0"
}, },
"dependencies": { "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": { "domelementtype": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
@ -24933,15 +24839,15 @@
"dev": true "dev": true
}, },
"eslint": { "eslint": {
"version": "8.38.0", "version": "8.39.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
"integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.2", "@eslint/eslintrc": "^2.0.2",
"@eslint/js": "8.38.0", "@eslint/js": "8.39.0",
"@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@ -24951,7 +24857,7 @@
"debug": "^4.3.2", "debug": "^4.3.2",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1", "eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.0", "eslint-visitor-keys": "^3.4.0",
"espree": "^9.5.1", "espree": "^9.5.1",
"esquery": "^1.4.2", "esquery": "^1.4.2",
@ -25454,12 +25360,6 @@
"dev": true, "dev": true,
"requires": {} "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": { "eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@ -25470,12 +25370,6 @@
"estraverse": "^4.1.1" "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": { "espree": {
"version": "9.5.1", "version": "9.5.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
@ -26647,15 +26541,15 @@
} }
}, },
"html-tags": { "html-tags": {
"version": "3.2.0", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
"dev": true "dev": true
}, },
"html-webpack-plugin": { "html-webpack-plugin": {
"version": "5.5.0", "version": "5.5.1",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz",
"integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/html-minifier-terser": "^6.0.0", "@types/html-minifier-terser": "^6.0.0",
@ -27377,9 +27271,9 @@
"dev": true "dev": true
}, },
"jassub": { "jassub": {
"version": "1.5.12", "version": "1.5.13",
"resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.13.tgz",
"integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", "integrity": "sha512-mQM88BcYgppvpPG6VE+DPQm7r6QS65EBedbm13RE4lRIhdrnQ+ihWhBOZXYZe3SlGhg+ROIDRK8uY4dm9ER2XQ==",
"requires": { "requires": {
"rvfc-polyfill": "^1.0.4" "rvfc-polyfill": "^1.0.4"
} }
@ -28120,9 +28014,9 @@
"optional": true "optional": true
}, },
"nanoid": { "nanoid": {
"version": "3.3.4", "version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true "dev": true
}, },
"nanomatch": { "nanomatch": {
@ -28767,12 +28661,12 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "8.4.21", "version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
@ -29565,16 +29459,16 @@
} }
}, },
"postcss-preset-env": { "postcss-preset-env": {
"version": "8.3.1", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.1.tgz", "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.3.2.tgz",
"integrity": "sha512-k3Y8BXbVLBAufrla3CNmQJhMS1iRuT9LFlysYvzs1rU5E78+ShX2u0EUL6KpMi0pDJO3wZcuVYSR8cgukfoRtg==", "integrity": "sha512-VSAOsfxTXzO/gX5QljC8x8hN3ABbD9iqqLgqHqohBdNI5FhJptwpl96kpu+kYvvzK7BWwaHYou0IeYrp+NqmcQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-cascade-layers": "^3.0.1",
"@csstools/postcss-color-function": "^2.2.1", "@csstools/postcss-color-function": "^2.2.1",
"@csstools/postcss-color-mix-function": "^1.0.1", "@csstools/postcss-color-mix-function": "^1.0.1",
"@csstools/postcss-font-format-keywords": "^2.0.2", "@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-hwb-function": "^2.2.1",
"@csstools/postcss-ic-unit": "^2.0.2", "@csstools/postcss-ic-unit": "^2.0.2",
"@csstools/postcss-is-pseudo-class": "^3.2.0", "@csstools/postcss-is-pseudo-class": "^3.2.0",
@ -30437,9 +30331,9 @@
"dev": true "dev": true
}, },
"sass": { "sass": {
"version": "1.62.0", "version": "1.62.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
"integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
"dev": true, "dev": true,
"requires": { "requires": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
@ -31325,14 +31219,14 @@
} }
}, },
"stylelint": { "stylelint": {
"version": "15.4.0", "version": "15.6.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.4.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.0.tgz",
"integrity": "sha512-TlOvpG3MbcFwHmK0q2ykhmpKo7Dq892beJit0NPdpyY9b1tFah/hGhqnAz/bRm2PDhDbJLKvjzkEYYBEz7Dxcg==", "integrity": "sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@csstools/css-parser-algorithms": "^2.1.0", "@csstools/css-parser-algorithms": "^2.1.1",
"@csstools/css-tokenizer": "^2.1.0", "@csstools/css-tokenizer": "^2.1.1",
"@csstools/media-query-list-parser": "^2.0.1", "@csstools/media-query-list-parser": "^2.0.4",
"@csstools/selector-specificity": "^2.2.0", "@csstools/selector-specificity": "^2.2.0",
"balanced-match": "^2.0.0", "balanced-match": "^2.0.0",
"colord": "^2.9.3", "colord": "^2.9.3",
@ -31346,7 +31240,7 @@
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^11.1.0", "globby": "^11.1.0",
"globjoin": "^0.1.4", "globjoin": "^0.1.4",
"html-tags": "^3.2.0", "html-tags": "^3.3.1",
"ignore": "^5.2.4", "ignore": "^5.2.4",
"import-lazy": "^4.0.0", "import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
@ -31357,7 +31251,7 @@
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"postcss": "^8.4.21", "postcss": "^8.4.22",
"postcss-media-query-parser": "^0.2.3", "postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1", "postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0", "postcss-safe-parser": "^6.0.0",
@ -33435,11 +33329,10 @@
} }
}, },
"swiper": { "swiper": {
"version": "8.4.7", "version": "9.2.4",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.2.4.tgz",
"integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", "integrity": "sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==",
"requires": { "requires": {
"dom7": "^4.0.4",
"ssr-window": "^4.0.2" "ssr-window": "^4.0.2"
} }
}, },
@ -34327,9 +34220,9 @@
} }
}, },
"webpack-dev-server": { "webpack-dev-server": {
"version": "4.13.2", "version": "4.13.3",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz",
"integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/bonjour": "^3.5.9", "@types/bonjour": "^3.5.9",

View file

@ -6,8 +6,6 @@
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.4", "@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-class-properties": "7.18.6",
"@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-transform-modules-umd": "7.18.6", "@babel/plugin-transform-modules-umd": "7.18.6",
@ -17,9 +15,9 @@
"@types/loadable__component": "5.13.4", "@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.7", "@types/lodash-es": "4.17.7",
"@types/react": "17.0.58", "@types/react": "17.0.58",
"@types/react-dom": "17.0.19", "@types/react-dom": "17.0.20",
"@typescript-eslint/eslint-plugin": "5.58.0", "@typescript-eslint/eslint-plugin": "5.59.1",
"@typescript-eslint/parser": "5.58.0", "@typescript-eslint/parser": "5.59.1",
"@uupaa/dynamic-import-polyfill": "1.0.2", "@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
@ -31,7 +29,7 @@
"css-loader": "6.7.3", "css-loader": "6.7.3",
"cssnano": "6.0.0", "cssnano": "6.0.0",
"es-check": "7.1.1", "es-check": "7.1.1",
"eslint": "8.38.0", "eslint": "8.39.0",
"eslint-plugin-compat": "4.1.4", "eslint-plugin-compat": "4.1.4",
"eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
@ -42,17 +40,17 @@
"eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-sonarjs": "0.19.0",
"expose-loader": "4.1.0", "expose-loader": "4.1.0",
"html-loader": "4.2.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", "mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.21", "postcss": "8.4.23",
"postcss-loader": "7.2.4", "postcss-loader": "7.2.4",
"postcss-preset-env": "8.3.1", "postcss-preset-env": "8.3.2",
"postcss-scss": "4.0.6", "postcss-scss": "4.0.6",
"sass": "1.62.0", "sass": "1.62.1",
"sass-loader": "13.2.2", "sass-loader": "13.2.2",
"source-map-loader": "4.0.1", "source-map-loader": "4.0.1",
"style-loader": "3.3.2", "style-loader": "3.3.2",
"stylelint": "15.4.0", "stylelint": "15.6.0",
"stylelint-config-rational-order": "0.1.2", "stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1", "stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
@ -61,7 +59,7 @@
"typescript": "5.0.4", "typescript": "5.0.4",
"webpack": "5.79.0", "webpack": "5.79.0",
"webpack-cli": "5.0.1", "webpack-cli": "5.0.1",
"webpack-dev-server": "4.13.2", "webpack-dev-server": "4.13.3",
"webpack-merge": "5.8.0", "webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.4", "workbox-webpack-plugin": "6.5.4",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
@ -90,7 +88,7 @@
"history": "5.3.0", "history": "5.3.0",
"hls.js": "1.4.0", "hls.js": "1.4.0",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"jassub": "1.5.12", "jassub": "1.5.13",
"jellyfin-apiclient": "1.10.0", "jellyfin-apiclient": "1.10.0",
"jquery": "3.6.4", "jquery": "3.6.4",
"jstree": "3.3.15", "jstree": "3.3.15",
@ -106,7 +104,7 @@
"resize-observer-polyfill": "1.5.1", "resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"swiper": "8.4.7", "swiper": "9.2.4",
"webcomponents.js": "0.7.24", "webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.2", "whatwg-fetch": "3.6.2",
"workbox-core": "6.5.4", "workbox-core": "6.5.4",

View file

@ -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 (
<ApiProvider>
<HistoryRouter history={history}>
<div className='backdropContainer' />
<div className='backgroundContainer' />
<div className='mainDrawer hide'>
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
</div>
<div className='skinHeader focuscontainer-x' />
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
{layoutMode === 'experimental' ? <ExperimentalAppRoutes /> : <AppRoutes /> }
</div>
<div className='mainDrawerHandle' />
</HistoryRouter>
</ApiProvider>
);
};
export default App;

30
src/RootApp.tsx Normal file
View file

@ -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 (
<ApiProvider>
<WebConfigProvider>
<HistoryRouter history={history}>
{
layoutMode === 'experimental' ?
<ExperimentalApp /> :
<StableApp />
}
</HistoryRouter>
</WebConfigProvider>
</ApiProvider>
);
};
export default RootApp;

2
src/apiclient.d.ts vendored
View file

@ -268,7 +268,7 @@ declare module 'jellyfin-apiclient' {
sendWebSocketMessage(name: string, data: any): void; sendWebSocketMessage(name: string, data: any): void;
serverAddress(val?: string): string; serverAddress(val?: string): string;
serverId(): string; serverId(): string;
serverVersion(): string serverVersion(): string;
setAuthenticationInfo(accessKey?: string, userId?: string): void; setAuthenticationInfo(accessKey?: string, userId?: string): void;
setRequestHeaders(headers: any): void; setRequestHeaders(headers: any): void;
setSystemInfo(info: SystemInfo): void; setSystemInfo(info: SystemInfo): void;

View file

@ -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 = () => (
<>
<Backdrop />
<AppHeader />
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
<ExperimentalAppRoutes />
</div>
</>
);
export default ExperimentalApp;

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../../components/ConnectionRequired'; import ConnectionRequired from '../../../components/ConnectionRequired';
import ServerContentPage from '../../components/ServerContentPage'; import ServerContentPage from '../../../components/ServerContentPage';
import { toAsyncPageRoute } from '../AsyncRoute'; import { toAsyncPageRoute } from '../../../components/router/AsyncRoute';
import { toViewManagerPageRoute } from '../LegacyRoute'; import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';

View file

@ -1,4 +1,4 @@
import { AsyncRoute } from '../../AsyncRoute'; import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'usernew.html', page: 'user/usernew' }, { path: 'usernew.html', page: 'user/usernew' },

View file

@ -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 }
];

View file

@ -1,20 +1,20 @@
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import globalize from '../scripts/globalize'; import globalize from '../../../scripts/globalize';
import LibraryMenu from '../scripts/libraryMenu'; import LibraryMenu from '../../../scripts/libraryMenu';
import { clearBackdrop } from '../components/backdrop/backdrop'; import { clearBackdrop } from '../../../components/backdrop/backdrop';
import layoutManager from '../components/layoutManager'; import layoutManager from '../../../components/layoutManager';
import * as mainTabsManager from '../components/maintabsmanager'; import * as mainTabsManager from '../../../components/maintabsmanager';
import '../elements/emby-tabs/emby-tabs'; import '../../../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button'; import '../../../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller'; import '../../../elements/emby-scroller/emby-scroller';
import Page from '../components/Page'; import Page from '../../../components/Page';
type OnResumeOptions = { type OnResumeOptions = {
autoFocus?: boolean; autoFocus?: boolean;
refresh?: boolean refresh?: boolean
} };
type ControllerProps = { type ControllerProps = {
onResume: ( onResume: (
@ -23,7 +23,7 @@ type ControllerProps = {
refreshed: boolean; refreshed: boolean;
onPause: () => void; onPause: () => void;
destroy: () => void; destroy: () => void;
} };
const Home: FunctionComponent = () => { const Home: FunctionComponent = () => {
const [ searchParams ] = useSearchParams(); const [ searchParams ] = useSearchParams();
@ -65,7 +65,7 @@ const Home: FunctionComponent = () => {
depends = 'favorites'; depends = 'favorites';
} }
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => { return import(/* webpackChunkName: "[request]" */ `../../../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = tabControllers[index]; let controller = tabControllers[index];
if (!controller) { if (!controller) {
@ -101,6 +101,8 @@ const Home: FunctionComponent = () => {
controller.refreshed = true; controller.refreshed = true;
tabController.current = controller; tabController.current = controller;
}).catch(err => {
console.error('[Home] failed to get tab controller', err);
}); });
}, [ getTabController ]); }, [ getTabController ]);

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{ {

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
{ {

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_USER_ROUTES: LegacyRoute[] = [ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
{ {

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../components/common/ViewItemsContainer'; import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => { const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => { const getBasekey = useCallback(() => {

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../components/common/ViewItemsContainer'; import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => { const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => { const getBasekey = useCallback(() => {

View file

@ -1,9 +1,9 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useState } from 'react'; import React, { FC, useCallback, useEffect, useState } from 'react';
import loading from '../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import GenresItemsContainer from '../../components/common/GenresItemsContainer'; import GenresItemsContainer from '../../../../components/common/GenresItemsContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => { const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({}); const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
@ -23,6 +23,8 @@ const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
).then((result) => { ).then((result) => {
setItemsResult(result); setItemsResult(result);
loading.hide(); loading.hide();
}).catch(err => {
console.error('[GenresView] failed to fetch genres', err);
}); });
}, [topParentId]); }, [topParentId]);

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../components/common/ViewItemsContainer'; import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => { const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => { const getBasekey = useCallback(() => {

View file

@ -1,13 +1,13 @@
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client'; import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import layoutManager from '../../components/layoutManager'; import layoutManager from '../../../../components/layoutManager';
import loading from '../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import dom from '../../scripts/dom'; import dom from '../../../../scripts/dom';
import globalize from '../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import RecommendationContainer from '../../components/common/RecommendationContainer'; import RecommendationContainer from '../../../../components/common/RecommendationContainer';
import SectionContainer from '../../components/common/SectionContainer'; import SectionContainer from '../../../../components/common/SectionContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => { const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]); const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
@ -28,8 +28,10 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
}, [enableScrollX]); }, [enableScrollX]);
const autoFocus = useCallback((page) => { const autoFocus = useCallback((page) => {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => { import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}).catch(err => {
console.error('[SuggestionsView] failed to load data', err);
}); });
}, []); }, []);
@ -55,6 +57,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
loading.hide(); loading.hide();
autoFocus(page); autoFocus(page);
}).catch(err => {
console.error('[SuggestionsView] failed to fetch items', err);
}); });
}, [autoFocus]); }, [autoFocus]);
@ -72,6 +76,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
setLatestItems(items); setLatestItems(items);
autoFocus(page); autoFocus(page);
}).catch(err => {
console.error('[SuggestionsView] failed to fetch latest items', err);
}); });
}, [autoFocus]); }, [autoFocus]);
@ -95,6 +101,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
setRecommendations(result); setRecommendations(result);
autoFocus(page); autoFocus(page);
}).catch(err => {
console.error('[SuggestionsView] failed to fetch recommendations', err);
}); });
}, [autoFocus]); }, [autoFocus]);

View file

@ -1,8 +1,8 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../components/common/ViewItemsContainer'; import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../types/interface'; import { LibraryViewProps } from '../../../../types/interface';
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => { const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => { const getBasekey = useCallback(() => {

View file

@ -1,16 +1,16 @@
import '../../elements/emby-scroller/emby-scroller'; import '../../../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import '../../../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-tabs/emby-tabs'; import '../../../../elements/emby-tabs/emby-tabs';
import '../../elements/emby-button/emby-button'; import '../../../../elements/emby-button/emby-button';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import * as mainTabsManager from '../../components/maintabsmanager'; import * as mainTabsManager from '../../../../components/maintabsmanager';
import Page from '../../components/Page'; import Page from '../../../../components/Page';
import globalize from '../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import libraryMenu from '../../scripts/libraryMenu'; import libraryMenu from '../../../../scripts/libraryMenu';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../../../scripts/settings/userSettings';
import CollectionsView from './CollectionsView'; import CollectionsView from './CollectionsView';
import FavoritesView from './FavoritesView'; import FavoritesView from './FavoritesView';
import GenresView from './GenresView'; import GenresView from './GenresView';
@ -114,6 +114,8 @@ const Movies: FC = () => {
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => { window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
page.setAttribute('data-title', item.Name as string); page.setAttribute('data-title', item.Name as string);
libraryMenu.setTitle(item.Name); libraryMenu.setTitle(item.Name);
}).catch(err => {
console.error('[movies] failed to fetch library', err);
}); });
} else { } else {
page.setAttribute('data-title', globalize.translate('Movies')); page.setAttribute('data-title', globalize.translate('Movies'));

19
src/apps/stable/App.tsx Normal file
View file

@ -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 = () => (
<>
<Backdrop />
<AppHeader />
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
<AppRoutes />
</div>
</>
);
export default StableApp;

View file

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../../components/ConnectionRequired'; import ConnectionRequired from '../../../components/ConnectionRequired';
import ServerContentPage from '../../components/ServerContentPage'; import ServerContentPage from '../../../components/ServerContentPage';
import { toAsyncPageRoute } from '../AsyncRoute'; import { toAsyncPageRoute } from '../../../components/router/AsyncRoute';
import { toViewManagerPageRoute } from '../LegacyRoute'; import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute';
import { ASYNC_USER_ROUTES } from './asyncRoutes'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
export const AppRoutes = () => ( export const AppRoutes = () => (
@ -19,6 +19,7 @@ export const AppRoutes = () => (
{/* Admin routes */} {/* Admin routes */}
<Route path='/' element={<ConnectionRequired isAdminRequired />}> <Route path='/' element={<ConnectionRequired isAdminRequired />}>
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
<Route path='configurationpage' element={ <Route path='configurationpage' element={

View file

@ -0,0 +1,10 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'usernew.html', page: 'user/usernew' },
{ path: 'userprofiles.html', page: 'user/userprofiles' },
{ path: 'useredit.html', page: 'user/useredit' },
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
{ path: 'userpassword.html', page: 'user/userpassword' }
];

View file

@ -0,0 +1,2 @@
export * from './admin';
export * from './user';

View file

@ -0,0 +1,6 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
{ path: 'search.html', page: 'search' },
{ path: 'userprofile.html', page: 'user/userprofile' }
];

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{ {
@ -193,41 +193,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
view: 'dashboard/streaming.html', view: 'dashboard/streaming.html',
controller: 'dashboard/streaming' controller: 'dashboard/streaming'
} }
}, {
path: 'usernew.html',
pageProps: {
view: 'dashboard/users/usernew.html',
controller: 'dashboard/users/usernew'
}
}, {
path: 'userprofiles.html',
pageProps: {
view: 'dashboard/users/userprofiles.html',
controller: 'dashboard/users/userprofilespage'
}
}, {
path: 'useredit.html',
pageProps: {
view: 'dashboard/users/useredit.html',
controller: 'dashboard/users/useredit'
}
}, {
path: 'userlibraryaccess.html',
pageProps: {
view: 'dashboard/users/userlibraryaccess.html',
controller: 'dashboard/users/userlibraryaccess'
}
}, {
path: 'userparentalcontrol.html',
pageProps: {
view: 'dashboard/users/userparentalcontrol.html',
controller: 'dashboard/users/userparentalcontrol'
}
}, {
path: 'userpassword.html',
pageProps: {
view: 'dashboard/users/userpassword.html',
controller: 'dashboard/users/userpasswordpage'
}
} }
]; ];

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
{ {

View file

@ -1,4 +1,4 @@
import { LegacyRoute } from '../../LegacyRoute'; import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_USER_ROUTES: LegacyRoute[] = [ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
{ {
@ -92,12 +92,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
isNowPlayingBarEnabled: false, isNowPlayingBarEnabled: false,
isThemeMediaSupported: true isThemeMediaSupported: true
} }
}, {
path: 'userprofile.html',
pageProps: {
controller: 'user/profile/index',
view: 'user/profile/index.html'
}
}, { }, {
path: 'home.html', path: 'home.html',
pageProps: { pageProps: {

View file

@ -1,12 +1,12 @@
import React, { FunctionComponent, useState } from 'react'; import React, { FunctionComponent, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import Page from '../components/Page'; import Page from '../../../components/Page';
import SearchFields from '../components/search/SearchFields'; import SearchFields from '../../../components/search/SearchFields';
import SearchResults from '../components/search/SearchResults'; import SearchResults from '../../../components/search/SearchResults';
import SearchSuggestions from '../components/search/SearchSuggestions'; import SearchSuggestions from '../../../components/search/SearchSuggestions';
import LiveTVSearchResults from '../components/search/LiveTVSearchResults'; import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults';
import globalize from '../scripts/globalize'; import globalize from '../../../scripts/globalize';
const Search: FunctionComponent = () => { const Search: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>(); const [ query, setQuery ] = useState<string>();

View file

@ -1,28 +1,43 @@
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client'; import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; 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 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 & { type ResetProvider = AuthProvider & {
checkedAttribute: string checkedAttribute: string
} };
type AuthProvider = { type AuthProvider = {
Name?: string; Name?: string;
Id?: string; Id?: string;
};
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
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 = () => { const UserEdit: FunctionComponent = () => {
@ -56,7 +71,7 @@ const UserEdit: FunctionComponent = () => {
} }
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement; 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); setAuthProviders(providers);
@ -73,7 +88,7 @@ const UserEdit: FunctionComponent = () => {
} }
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement; 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); setPasswordResetProviders(providers);
@ -121,6 +136,8 @@ const UserEdit: FunctionComponent = () => {
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement; const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion; chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders); 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) { window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(user, 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) { window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(user, providers); loadPasswordResetProviders(user, providers);
}).catch(err => {
console.error('[useredit] failed to fetch password reset providers', err);
}); });
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false IsHidden: false
})).then(function (folders) { })).then(function (folders) {
loadDeleteFolders(user, folders.Items); loadDeleteFolders(user, folders.Items);
}).catch(err => {
console.error('[useredit] failed to fetch media folders', err);
}); });
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; 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; const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false; txtUserName.disabled = false;
@ -185,6 +208,8 @@ const UserEdit: FunctionComponent = () => {
loading.show(); loading.show();
getUser().then(function (user) { getUser().then(function (user) {
loadUser(user); loadUser(user);
}).catch(err => {
console.error('[useredit] failed to load data', err);
}); });
}, [loadUser]); }, [loadUser]);
@ -198,19 +223,9 @@ const UserEdit: FunctionComponent = () => {
loadData(); loadData();
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const saveUser = (user: UserDto) => { const saveUser = (user: UserDto) => {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value; 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.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value; user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked; user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) { user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
return c.checked; user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
}).map(function (c) {
return c.getAttribute('data-id'); window.ApiClient.updateUser(user).then(() => (
}); window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
if (window.ApiClient.isMinServerVersion('10.6.0')) { )).then(() => {
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; onSaveComplete();
} }).catch(err => {
window.ApiClient.updateUser(user).then(function () { console.error('[useredit] failed to update user', err);
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
onSaveComplete();
});
}); });
}; };
@ -254,6 +266,8 @@ const UserEdit: FunctionComponent = () => {
loading.show(); loading.show();
getUser().then(function (result) { getUser().then(function (result) {
saveUser(result); saveUser(result);
}).catch(err => {
console.error('[useredit] failed to fetch user', err);
}); });
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -261,16 +275,13 @@ const UserEdit: FunctionComponent = () => {
}; };
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) { (page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
}
}); });
window.ApiClient.getNamedConfiguration('network').then(function (config) { window.ApiClient.getNamedConfiguration('network').then(function (config) {
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement; (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide'); }).catch(err => {
console.error('[useredit] failed to load network config', err);
}); });
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);

View file

@ -1,24 +1,24 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu'; import libraryMenu from '../../../../scripts/libraryMenu';
import globalize from '../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import toast from '../../components/toast/toast'; import toast from '../../../../components/toast/toast';
import SectionTabs from '../../components/dashboard/users/SectionTabs'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import ButtonElement from '../../elements/ButtonElement'; import ButtonElement from '../../../../elements/ButtonElement';
import { getParameterByName } from '../../utils/url'; import { getParameterByName } from '../../../../utils/url';
import SectionTitleContainer from '../../elements/SectionTitleContainer'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import AccessContainer from '../../components/dashboard/users/AccessContainer'; import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement'; import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../components/Page'; import Page from '../../../../components/Page';
type ItemsArr = { type ItemsArr = {
Name?: string; Name?: string;
Id?: string; Id?: string;
AppName?: string; AppName?: string;
checkedAttribute?: string checkedAttribute?: string
} };
const UserLibraryAccess: FunctionComponent = () => { const UserLibraryAccess: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
@ -148,6 +148,8 @@ const UserLibraryAccess: FunctionComponent = () => {
const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices')); const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices'));
Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) { Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) {
loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items); loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items);
}).catch(err => {
console.error('[userlibraryaccess] failed to load data', err);
}); });
}, [loadUser]); }, [loadUser]);
@ -166,6 +168,8 @@ const UserLibraryAccess: FunctionComponent = () => {
const userId = getParameterByName('userId'); const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) { window.ApiClient.getUser(userId).then(function (result) {
saveUser(result); saveUser(result);
}).catch(err => {
console.error('[userlibraryaccess] failed to fetch user', err);
}); });
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -203,6 +207,8 @@ const UserLibraryAccess: FunctionComponent = () => {
user.Policy.BlockedMediaFolders = null; user.Policy.BlockedMediaFolders = null;
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete(); onSaveComplete();
}).catch(err => {
console.error('[userlibraryaccess] failed to update user policy', err);
}); });
}; };

View file

@ -1,25 +1,25 @@
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import loading from '../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import toast from '../../components/toast/toast'; import toast from '../../../../components/toast/toast';
import SectionTitleContainer from '../../elements/SectionTitleContainer'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import InputElement from '../../elements/InputElement'; import InputElement from '../../../../elements/InputElement';
import ButtonElement from '../../elements/ButtonElement'; import ButtonElement from '../../../../elements/ButtonElement';
import AccessContainer from '../../components/dashboard/users/AccessContainer'; import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement'; import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../components/Page'; import Page from '../../../../components/Page';
type userInput = { type userInput = {
Name?: string; Name?: string;
Password?: string; Password?: string;
} };
type ItemsArr = { type ItemsArr = {
Name?: string; Name?: string;
Id?: string; Id?: string;
} };
const UserNew: FunctionComponent = () => { const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]); const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
@ -93,6 +93,8 @@ const UserNew: FunctionComponent = () => {
loadMediaFolders(responses[0].Items); loadMediaFolders(responses[0].Items);
loadChannels(responses[1].Items); loadChannels(responses[1].Items);
loading.hide(); loading.hide();
}).catch(err => {
console.error('[usernew] failed to load data', err);
}); });
}, [loadChannels, loadMediaFolders]); }, [loadChannels, loadMediaFolders]);
@ -111,12 +113,8 @@ const UserNew: FunctionComponent = () => {
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value; userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value; userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
window.ApiClient.createUser(userInput).then(function (user) { window.ApiClient.createUser(userInput).then(function (user) {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked; 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 () { 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 () { }, function () {
toast(globalize.translate('ErrorDefault')); toast(globalize.translate('ErrorDefault'));

View file

@ -1,26 +1,27 @@
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client'; 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 { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; 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 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 = { type UnratedItem = {
name: string; name: string;
value: string; value: string;
checkedAttribute: string checkedAttribute: string
} };
const UserParentalControl: FunctionComponent = () => { const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
@ -196,6 +197,8 @@ const UserParentalControl: FunctionComponent = () => {
const promise2 = window.ApiClient.getParentalRatings(); const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) { Promise.all([promise1, promise2]).then(function (responses) {
loadUser(responses[0], responses[1]); loadUser(responses[0], responses[1]);
}).catch(err => {
console.error('[userparentalcontrol] failed to load data', err);
}); });
}, [loadUser]); }, [loadUser]);
@ -215,12 +218,8 @@ const UserParentalControl: FunctionComponent = () => {
}; };
const saveUser = (user: UserDto) => { const saveUser = (user: UserDto) => {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
@ -234,12 +233,14 @@ const UserParentalControl: FunctionComponent = () => {
user.Policy.BlockedTags = getBlockedTagsFromPage(); user.Policy.BlockedTags = getBlockedTagsFromPage();
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete(); onSaveComplete();
}).catch(err => {
console.error('[userparentalcontrol] failed to update user policy', err);
}); });
}; };
const showSchedulePopup = (schedule: AccessSchedule, index: number) => { const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {}; schedule = schedule || {};
import('../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
accessschedule.show({ accessschedule.show({
schedule: schedule schedule: schedule
}).then(function (updatedSchedule) { }).then(function (updatedSchedule) {
@ -251,7 +252,11 @@ const UserParentalControl: FunctionComponent = () => {
schedules[index] = updatedSchedule; schedules[index] = updatedSchedule;
renderAccessSchedule(schedules); 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 = () => { const showBlockedTagPopup = () => {
import('../../components/prompt/prompt').then(({ default: prompt }) => { import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
label: globalize.translate('LabelTag') label: globalize.translate('LabelTag')
}).then(function (value) { }).then(function (value) {
@ -282,7 +287,11 @@ const UserParentalControl: FunctionComponent = () => {
tags.push(value); tags.push(value);
loadBlockedTags(tags); 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'); const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) { window.ApiClient.getUser(userId).then(function (result) {
saveUser(result); saveUser(result);
}).catch(err => {
console.error('[userparentalcontrol] failed to fetch user', err);
}); });
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -1,10 +1,11 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import { getParameterByName } from '../../utils/url'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import SectionTitleContainer from '../../elements/SectionTitleContainer'; import { getParameterByName } from '../../../../utils/url';
import Page from '../../components/Page'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import loading from '../../components/loading/loading'; import Page from '../../../../components/Page';
import loading from '../../../../components/loading/loading';
const UserPassword: FunctionComponent = () => { const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId'); const userId = getParameterByName('userId');
@ -18,6 +19,8 @@ const UserPassword: FunctionComponent = () => {
} }
setUserName(user.Name); setUserName(user.Name);
loading.hide(); loading.hide();
}).catch(err => {
console.error('[userpassword] failed to fetch user', err);
}); });
}, [userId]); }, [userId]);
useEffect(() => { useEffect(() => {

View file

@ -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 { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react'; import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu'; import LibraryMenu from '../../../../scripts/libraryMenu';
import { appHost } from '../../components/apphost'; import { appHost } from '../../../../components/apphost';
import confirm from '../../components/confirm/confirm'; import confirm from '../../../../components/confirm/confirm';
import ButtonElement from '../../elements/ButtonElement'; import ButtonElement from '../../../../elements/ButtonElement';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import loading from '../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import toast from '../../components/toast/toast'; import toast from '../../../../components/toast/toast';
import { getParameterByName } from '../../utils/url'; import { getParameterByName } from '../../../../utils/url';
import Page from '../../components/Page'; import Page from '../../../../components/Page';
const UserProfile: FunctionComponent = () => { const UserProfile: FunctionComponent = () => {
const userId = getParameterByName('userId'); const userId = getParameterByName('userId');
@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => {
loading.show(); loading.show();
window.ApiClient.getUser(userId).then(function (user) { window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) { if (!user.Name || !user.Id) {
throw new Error('Unexpected null user.Name'); throw new Error('Unexpected null user name or id');
}
if (!user.Id) {
throw new Error('Unexpected null user.Id');
} }
setUserName(user.Name); setUserName(user.Name);
@ -63,8 +59,12 @@ const UserProfile: FunctionComponent = () => {
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
} }
}).catch(err => {
console.error('[userprofile] failed to get current user', err);
}); });
loading.hide(); loading.hide();
}).catch(err => {
console.error('[userprofile] failed to load data', err);
}); });
}, [userId]); }, [userId]);
@ -114,6 +114,8 @@ const UserProfile: FunctionComponent = () => {
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () { window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
loading.hide(); loading.hide();
reloadUser(); 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 () { window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () {
loading.hide(); loading.hide();
reloadUser(); reloadUser();
}).catch(err => {
console.error('[userprofile] failed to delete image', err);
}); });
}).catch(() => {
// confirm dialog closed
}); });
}); });

View file

@ -1,24 +1,25 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize'; import Dashboard from '../../../../utils/dashboard';
import loading from '../../components/loading/loading'; import globalize from '../../../../scripts/globalize';
import dom from '../../scripts/dom'; import loading from '../../../../components/loading/loading';
import confirm from '../../components/confirm/confirm'; import dom from '../../../../scripts/dom';
import UserCardBox from '../../components/dashboard/users/UserCardBox'; import confirm from '../../../../components/confirm/confirm';
import SectionTitleContainer from '../../elements/SectionTitleContainer'; import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
import '../../elements/emby-button/emby-button'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import '../../elements/emby-button/paper-icon-button-light'; import '../../../../elements/emby-button/emby-button';
import '../../components/cardbuilder/card.scss'; import '../../../../elements/emby-button/paper-icon-button-light';
import '../../components/indicators/indicators.scss'; import '../../../../components/cardbuilder/card.scss';
import '../../styles/flexstyles.scss'; import '../../../../components/indicators/indicators.scss';
import Page from '../../components/Page'; import '../../../../styles/flexstyles.scss';
import Page from '../../../../components/Page';
type MenuEntry = { type MenuEntry = {
name?: string; name?: string;
id?: string; id?: string;
icon?: string; icon?: string;
} };
const UserProfiles: FunctionComponent = () => { const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]); const [ users, setUsers ] = useState<UserDto[]>([]);
@ -30,6 +31,8 @@ const UserProfiles: FunctionComponent = () => {
window.ApiClient.getUsers().then(function (result) { window.ApiClient.getUsers().then(function (result) {
setUsers(result); setUsers(result);
loading.hide(); loading.hide();
}).catch(err => {
console.error('[userprofiles] failed to fetch users', err);
}); });
}; };
@ -75,29 +78,42 @@ const UserProfiles: FunctionComponent = () => {
icon: 'delete' icon: 'delete'
}); });
import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => { import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: menuItems, items: menuItems,
positionTo: card, positionTo: card,
callback: function (id: string) { callback: function (id: string) {
switch (id) { switch (id) {
case 'open': 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; break;
case 'access': 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; break;
case 'parentalcontrol': 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; break;
case 'delete': case 'delete':
deleteUser(userId); 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(); loading.show();
window.ApiClient.deleteUser(id).then(function () { window.ApiClient.deleteUser(id).then(function () {
loadData(); 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() { (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);
});
}); });
}, []); }, []);

View file

@ -0,0 +1,20 @@
import React, { useEffect } from 'react';
const AppHeader = () => {
useEffect(() => {
// Initialize the UI components after first render
import('../scripts/libraryMenu');
}, []);
return (
<>
<div className='mainDrawer hide'>
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
</div>
<div className='skinHeader focuscontainer-x' />
<div className='mainDrawerHandle' />
</>
);
};
export default AppHeader;

View file

@ -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;

View file

@ -0,0 +1,17 @@
import React, { useEffect } from 'react';
const Backdrop = () => {
useEffect(() => {
// Initialize the UI components after first render
import('../scripts/autoBackdrops');
}, []);
return (
<>
<div className='backdropContainer' />
<div className='backgroundContainer' />
</>
);
};
export default Backdrop;

View file

@ -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 { Outlet, useLocation, useNavigate } from 'react-router-dom';
import type { ConnectResponse } from 'jellyfin-apiclient'; import type { ConnectResponse } from 'jellyfin-apiclient';
import alert from './alert'; import alert from './alert';
import { appRouter } from './appRouter'; import { appRouter } from './router/appRouter';
import Loading from './loading/LoadingComponent'; import Loading from './loading/LoadingComponent';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
@ -35,116 +35,134 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => { const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
const bounce = async (connectionResponse: ConnectResponse) => { switch (connectionResponse.State) {
switch (connectionResponse.State) { case ConnectionState.SignedIn:
case ConnectionState.SignedIn: // Already logged in, bounce to the home page
// Already logged in, bounce to the home page console.debug('[ConnectionRequired] already logged in, redirecting to home');
console.debug('[ConnectionRequired] already logged in, redirecting to home'); navigate(BounceRoutes.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', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
});
} 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; return;
case ConnectionState.ServerSignIn: }
// Bounce to the login page } catch (ex) {
if (location.pathname === BounceRoutes.Login) { console.error('[ConnectionRequired] checking wizard status failed', ex);
setIsLoading(false); return;
} else { }
console.debug('[ConnectionRequired] not logged in, redirecting to login page'); }
navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
} // Bounce to the correct page in the login flow
return; bounce(firstConnection)
case ConnectionState.ServerSelection: .catch(err => {
// Bounce to select server page console.error('[ConnectionRequired] failed to bounce', err);
console.debug('[ConnectionRequired] redirecting to select server page'); });
navigate(BounceRoutes.SelectServer); }, [bounce, navigate]);
return;
case ConnectionState.ServerUpdateNeeded: const validateUserAccess = useCallback(async () => {
// Show update needed message and bounce to select server page const client = ServerConnections.currentApiClient();
try {
await alert({ // If this is a user route, ensure a user is logged in
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'), if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>') 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; return;
} }
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
return;
} }
}
setIsLoading(false); setIsLoading(false);
}; }, [bounce, isAdminRequired, isUserRequired]);
validateConnection(); useEffect(() => {
}, [ isAdminRequired, isUserRequired, location.pathname, navigate ]); // 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) { if (isLoading) {
return <Loading />; return <Loading />;

View file

@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';
import viewManager from './viewManager/viewManager'; import viewManager from './viewManager/viewManager';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import type { RestoreViewFailResponse } from '../types/viewManager';
interface ServerContentPageProps { interface ServerContentPageProps {
view: string view: string
@ -29,7 +30,7 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
}; };
viewManager.tryRestoreView(viewOptions) viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => { .catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) { if (!result || !result.cancelled) {
const apiClient = ServerConnections.currentApiClient(); const apiClient = ServerConnections.currentApiClient();
@ -46,12 +47,13 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
}; };
loadPage(); 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, view,
location.pathname, location.pathname,
location.search location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]); ]);
return <></>; return <></>;

View file

@ -1,4 +1,4 @@
import { appRouter } from './appRouter'; import { appRouter } from './router/appRouter';
import browser from '../scripts/browser'; import browser from '../scripts/browser';
import dialog from './dialog/dialog'; import dialog from './dialog/dialog';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';

View file

@ -114,7 +114,7 @@ button::-moz-focus-inner {
} }
.card.show-animation:focus > .cardBox { .card.show-animation:focus > .cardBox {
transform: scale(1.18, 1.18); transform: scale(1.07, 1.07);
} }
.cardBox-bottompadded { .cardBox-bottompadded {

View file

@ -22,7 +22,7 @@ import './card.scss';
import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-button/paper-icon-button-light';
import '../guide/programs.scss'; import '../guide/programs.scss';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -679,9 +679,8 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout
let valid = 0; let valid = 0;
for (let i = 0; i < lines.length; i++) { for (const text of lines) {
let currentCssClass = cssClass; let currentCssClass = cssClass;
const text = lines[i];
if (valid > 0 && isOuterFooter) { if (valid > 0 && isOuterFooter) {
currentCssClass += ' cardText-secondary'; currentCssClass += ' cardText-secondary';
@ -862,8 +861,8 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
if (options.textLines) { if (options.textLines) {
const additionalLines = options.textLines(item); const additionalLines = options.textLines(item);
for (let i = 0; i < additionalLines.length; i++) { for (const additionalLine of additionalLines) {
lines.push(additionalLines[i]); lines.push(additionalLine);
} }
} }
@ -1118,7 +1117,6 @@ let refreshIndicatorLoaded;
function importRefreshIndicator() { function importRefreshIndicator() {
if (!refreshIndicatorLoaded) { if (!refreshIndicatorLoaded) {
refreshIndicatorLoaded = true; refreshIndicatorLoaded = true;
/* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator'); import('../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator');
} }
} }
@ -1469,7 +1467,6 @@ function getHoverMenuHtml(item, action) {
const userData = item.UserData || {}; const userData = item.UserData || {};
if (itemHelper.canMarkPlayed(item)) { if (itemHelper.canMarkPlayed(item)) {
/* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-playstatebutton/emby-playstatebutton'); import('../../elements/emby-playstatebutton/emby-playstatebutton');
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>'; html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>';
} }
@ -1477,7 +1474,6 @@ function getHoverMenuHtml(item, action) {
if (itemHelper.canRate(item)) { if (itemHelper.canRate(item)) {
const likes = userData.Likes == null ? '' : userData.Likes; const likes = userData.Likes == null ? '' : userData.Likes;
/* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-ratingbutton/emby-ratingbutton'); import('../../elements/emby-ratingbutton/emby-ratingbutton');
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>'; html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>';
} }
@ -1724,8 +1720,7 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) {
export function onTimerCancelled(timerId, itemsContainer) { export function onTimerCancelled(timerId, itemsContainer) {
const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]'); const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]');
for (let i = 0; i < cells.length; i++) { for (const cell of cells) {
const cell = cells[i];
const icon = cell.querySelector('.timerIndicator'); const icon = cell.querySelector('.timerIndicator');
if (icon) { if (icon) {
icon.parentNode.removeChild(icon); icon.parentNode.removeChild(icon);
@ -1742,8 +1737,7 @@ export function onTimerCancelled(timerId, itemsContainer) {
export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]'); const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]');
for (let i = 0; i < cells.length; i++) { for (const cell of cells) {
const cell = cells[i];
const icon = cell.querySelector('.timerIndicator'); const icon = cell.querySelector('.timerIndicator');
if (icon) { if (icon) {
icon.parentNode.removeChild(icon); icon.parentNode.removeChild(icon);

View file

@ -3,7 +3,7 @@ import dom from '../../scripts/dom';
import dialogHelper from '../dialogHelper/dialogHelper'; import dialogHelper from '../dialogHelper/dialogHelper';
import loading from '../loading/loading'; import loading from '../loading/loading';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/emby-button';
import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-button/paper-icon-button-light';

View file

@ -32,7 +32,11 @@ const Filter: FC<FilterProps> = ({
serverId: window.ApiClient.serverId(), serverId: window.ApiClient.serverId(),
filterMenuOptions: getFilterMenuOptions(), filterMenuOptions: getFilterMenuOptions(),
setfilters: setViewQuerySettings setfilters: setViewQuerySettings
}).catch(() => {
// filter menu closed
}); });
}).catch(err => {
console.error('[Filter] failed to load filter menu', err);
}); });
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]); }, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);

View file

@ -5,7 +5,7 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'
import escapeHTML from 'escape-html'; import escapeHTML from 'escape-html';
import React, { FC, useCallback, useEffect, useRef } from 'react'; import React, { FC, useCallback, useEffect, useRef } from 'react';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import cardBuilder from '../cardbuilder/cardBuilder'; import cardBuilder from '../cardbuilder/cardBuilder';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
@ -73,6 +73,8 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
centerText: true, centerText: true,
showYear: true showYear: true
}); });
}).catch(err => {
console.error('[GenresItemsContainer] failed to fetch items', err);
}); });
}, [getPortraitShape, topParentId]); }, [getPortraitShape, topParentId]);

View file

@ -12,7 +12,11 @@ const NewCollection: FC = () => {
collectionEditor.show({ collectionEditor.show({
items: [], items: [],
serverId: serverId serverId: serverId
}).catch(() => {
// closed collection editor
}); });
}).catch(err => {
console.error('[NewCollection] failed to load collection editor', err);
}); });
}, []); }, []);

View file

@ -22,7 +22,11 @@ const SelectView: FC<SelectViewProps> = ({
settings: viewQuerySettings, settings: viewQuerySettings,
visibleSettings: getVisibleViewSettings(), visibleSettings: getVisibleViewSettings(),
setviewsettings: setViewQuerySettings setviewsettings: setViewQuerySettings
}).catch(() => {
// view settings closed
}); });
}).catch(err => {
console.error('[SelectView] failed to load view settings', err);
}); });
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]); }, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);

View file

@ -18,6 +18,8 @@ const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
topParentId as string topParentId as string
).then((item) => { ).then((item) => {
playbackManager.shuffle(item); playbackManager.shuffle(item);
}).catch(err => {
console.error('[Shuffle] failed to fetch items', err);
}); });
}, [topParentId]); }, [topParentId]);

View file

@ -25,7 +25,11 @@ const Sort: FC<SortProps> = ({
settings: viewQuerySettings, settings: viewQuerySettings,
sortOptions: getSortMenuOptions(), sortOptions: getSortMenuOptions(),
setSortValues: setViewQuerySettings setSortValues: setViewQuerySettings
}).catch(() => {
// sort menu closed
}); });
}).catch(err => {
console.error('[Sort] failed to load sort menu', err);
}); });
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]); }, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);

View file

@ -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 React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import loading from '../loading/loading'; import loading from '../loading/loading';
@ -33,6 +33,41 @@ const getDefaultSortBy = () => {
return 'SortName'; 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 = () => { const getVisibleViewSettings = () => {
return [ return [
'showTitle', 'showTitle',
@ -228,33 +263,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
const getQuery = useCallback(() => { const getQuery = useCallback(() => {
let fields = 'BasicSyncInfo,MediaSourceCount'; const queryFilters = getFilters(viewQuerySettings);
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');
}
let queryIsHD; let queryIsHD;
@ -271,7 +280,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
SortOrder: viewQuerySettings.SortOrder, SortOrder: viewQuerySettings.SortOrder,
IncludeItemTypes: getItemTypes().join(','), IncludeItemTypes: getItemTypes().join(','),
Recursive: true, Recursive: true,
Fields: fields, Fields: getFields(viewQuerySettings),
ImageTypeLimit: 1, ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
Limit: userSettings.libraryPageSize(undefined) || undefined, Limit: userSettings.libraryPageSize(undefined) || undefined,
@ -293,28 +302,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
ParentId: topParentId ParentId: topParentId
}; };
}, [ }, [
viewQuerySettings.imageType, viewQuerySettings,
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,
getItemTypes, getItemTypes,
getBasekey, getBasekey,
topParentId topParentId
@ -347,9 +335,13 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
import('../../components/autoFocuser').then(({ default: autoFocuser }) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}).catch(err => {
console.error('[ViewItemsContainer] failed to load autofocuser', err);
}); });
loading.hide(); loading.hide();
setisLoading(true); setisLoading(true);
}).catch(err => {
console.error('[ViewItemsContainer] failed to fetch data', err);
}); });
}, [fetchData]); }, [fetchData]);

View file

@ -1,4 +1,4 @@
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import dialog from '../dialog/dialog'; import dialog from '../dialog/dialog';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';

View file

@ -12,7 +12,7 @@ type IProps = {
listTitle?: string; listTitle?: string;
description?: string; description?: string;
children?: React.ReactNode children?: React.ReactNode
} };
const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => { const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
return ( return (

View file

@ -9,7 +9,7 @@ type AccessScheduleListProps = {
DayOfWeek?: string; DayOfWeek?: string;
StartHour?: number ; StartHour?: number ;
EndHour?: number; EndHour?: number;
} };
function getDisplayTime(hours = 0) { function getDisplayTime(hours = 0) {
let minutes = 0; let minutes = 0;

View file

@ -3,7 +3,7 @@ import IconButtonElement from '../../../elements/IconButtonElement';
type IProps = { type IProps = {
tag?: string; tag?: string;
} };
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => { const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
return ( return (

View file

@ -4,7 +4,7 @@ import globalize from '../../../scripts/globalize';
type IProps = { type IProps = {
title?: string; title?: string;
className?: string; className?: string;
} };
const createLinkElement = ({ className, title }: IProps) => ({ const createLinkElement = ({ className, title }: IProps) => ({
__html: `<a __html: `<a

View file

@ -3,7 +3,7 @@ import globalize from '../../../scripts/globalize';
type IProps = { type IProps = {
activeTab: string; activeTab: string;
} };
const createLinkElement = (activeTab: string) => ({ const createLinkElement = (activeTab: string) => ({
__html: `<a href="#" __html: `<a href="#"

View file

@ -19,7 +19,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
type IProps = { type IProps = {
user?: UserDto; user?: UserDto;
} };
const getLastSeenText = (lastActivityDate?: string | null) => { const getLastSeenText = (lastActivityDate?: string | null) => {
if (lastActivityDate) { if (lastActivityDate) {

View file

@ -1,4 +1,3 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import Dashboard from '../../../utils/dashboard'; import Dashboard from '../../../utils/dashboard';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
@ -12,12 +11,12 @@ import InputElement from '../../../elements/InputElement';
type IProps = { type IProps = {
userId: string; userId: string;
} };
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => { const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const loadUser = useCallback(() => { const loadUser = useCallback(async () => {
const page = element.current; const page = element.current;
if (!page) { if (!page) {
@ -25,61 +24,50 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
return; return;
} }
window.ApiClient.getUser(userId).then(function (user) { const user = await window.ApiClient.getUser(userId);
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) { const loggedInUser = await Dashboard.getCurrentUser();
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
if (!user.Configuration) { if (!user.Policy || !user.Configuration) {
throw new Error('Unexpected null 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) { if (user.HasConfiguredPassword) {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true; showLocalAccessSection = true;
} else { } else {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
} }
if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) { const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess;
(page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide'); (page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword);
} else { (page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword));
(page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide');
}
if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide'); txtEasyPassword.value = '';
} else {
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide');
}
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement; if (user.HasConfiguredEasyPassword) {
txtEasyPassword.value = ''; 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) { const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
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; 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 }) => { }).catch(err => {
autoFocuser.autoFocus(page); console.error('[UserPasswordForm] failed to load autofocuser', err);
});
});
}); });
(page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = ''; (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = '';
@ -95,7 +83,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
return; return;
} }
loadUser(); loadUser().catch(err => {
console.error('[UserPasswordForm] failed to load user', err);
});
const onSubmit = (e: Event) => { const onSubmit = (e: Event) => {
if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) { if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) {
@ -123,7 +113,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
loading.hide(); loading.hide();
toast(globalize.translate('PasswordSaved')); toast(globalize.translate('PasswordSaved'));
loadUser(); loadUser().catch(err => {
console.error('[UserPasswordForm] failed to load user', err);
});
}, function () { }, function () {
loading.hide(); loading.hide();
Dashboard.alert({ Dashboard.alert({
@ -146,6 +138,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
if (easyPassword) { if (easyPassword) {
window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () { window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
onEasyPasswordSaved(); onEasyPasswordSaved();
}).catch(err => {
console.error('[UserPasswordForm] failed to update easy password', err);
}); });
} else { } else {
onEasyPasswordSaved(); onEasyPasswordSaved();
@ -167,8 +161,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
loading.hide(); loading.hide();
toast(globalize.translate('SettingsSaved')); 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<IProps> = ({ userId }: IProps) => {
message: globalize.translate('PinCodeResetComplete'), message: globalize.translate('PinCodeResetComplete'),
title: globalize.translate('HeaderPinCodeReset') 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<IProps> = ({ userId }: IProps) => {
message: globalize.translate('PasswordResetComplete'), message: globalize.translate('PasswordResetComplete'),
title: globalize.translate('ResetPassword') 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
}); });
}; };

View file

@ -1,4 +1,4 @@
import { history } from '../appRouter'; import { history } from '../router/appRouter';
import focusManager from '../focusManager'; import focusManager from '../focusManager';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';

View file

@ -1,5 +1,5 @@
import dom from '../scripts/dom'; import dom from '../scripts/dom';
import { appRouter } from './appRouter'; import { appRouter } from './router/appRouter';
import Dashboard from '../utils/dashboard'; import Dashboard from '../utils/dashboard';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';

View file

@ -3,7 +3,7 @@ import cardBuilder from '../cardbuilder/cardBuilder';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import imageLoader from '../images/imageLoader'; import imageLoader from '../images/imageLoader';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import imageHelper from '../../scripts/imagehelper'; import imageHelper from '../../scripts/imagehelper';
import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-button/paper-icon-button-light';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import '../../elements/emby-itemscontainer/emby-itemscontainer';
@ -100,10 +100,10 @@ export function loadSections(elem, apiClient, user, userSettings) {
export function destroySections(elem) { export function destroySections(elem) {
const elems = elem.querySelectorAll('.itemsContainer'); const elems = elem.querySelectorAll('.itemsContainer');
for (let i = 0; i < elems.length; i++) { for (const e of elems) {
elems[i].fetchData = null; e.fetchData = null;
elems[i].parentContainer = null; e.parentContainer = null;
elems[i].getItemsHtml = null; e.getItemsHtml = null;
} }
elem.innerHTML = ''; elem.innerHTML = '';
@ -111,8 +111,8 @@ export function destroySections(elem) {
export function pause(elem) { export function pause(elem) {
const elems = elem.querySelectorAll('.itemsContainer'); const elems = elem.querySelectorAll('.itemsContainer');
for (let i = 0; i < elems.length; i++) { for (const e of elems) {
elems[i].pause(); e.pause();
} }
} }

View file

@ -1,9 +1,10 @@
import browser from '../scripts/browser'; import browser from '../scripts/browser';
import { copy } from '../scripts/clipboard'; import { copy } from '../scripts/clipboard';
import dom from '../scripts/dom';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import actionsheet from './actionSheet/actionSheet'; import actionsheet from './actionSheet/actionSheet';
import { appHost } from './apphost'; import { appHost } from './apphost';
import { appRouter } from './appRouter'; import { appRouter } from './router/appRouter';
import itemHelper from './itemHelper'; import itemHelper from './itemHelper';
import { playbackManager } from './playback/playbackmanager'; import { playbackManager } from './playback/playbackmanager';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';
@ -98,6 +99,16 @@ export function getCommands(options) {
} }
if (!browser.tv) { 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) { if (itemHelper.supportsAddingToCollection(item) && options.EnableCollectionManagement) {
commands.push({ commands.push({
name: globalize.translate('AddToCollection'), name: globalize.translate('AddToCollection'),
@ -432,6 +443,12 @@ function executeCommand(item, id, options) {
itemMediaInfo.show(itemId, serverId).then(getResolveFunction(resolve, id), getResolveFunction(resolve, id)); itemMediaInfo.show(itemId, serverId).then(getResolveFunction(resolve, id), getResolveFunction(resolve, id));
}); });
break; break;
case 'multiSelect':
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
const card = dom.parentWithClass(options.positionTo, 'card');
startMultiSelect(card);
});
break;
case 'refresh': case 'refresh':
refresh(apiClient, item); refresh(apiClient, item);
getResolveFunction(resolve, id)(); getResolveFunction(resolve, id)();

View file

@ -23,8 +23,7 @@ function populateLanguages(parent) {
function populateLanguagesIntoSelect(select, languages) { function populateLanguagesIntoSelect(select, languages) {
let html = ''; let html = '';
html += "<option value=''></option>"; html += "<option value=''></option>";
for (let i = 0; i < languages.length; i++) { for (const culture of languages) {
const culture = languages[i];
html += `<option value='${culture.TwoLetterISOLanguageName}'>${culture.DisplayName}</option>`; html += `<option value='${culture.TwoLetterISOLanguageName}'>${culture.DisplayName}</option>`;
} }
select.innerHTML = html; select.innerHTML = html;
@ -32,8 +31,7 @@ function populateLanguagesIntoSelect(select, languages) {
function populateLanguagesIntoList(element, languages) { function populateLanguagesIntoList(element, languages) {
let html = ''; let html = '';
for (let i = 0; i < languages.length; i++) { for (const culture of languages) {
const culture = languages[i];
html += `<label><input type="checkbox" is="emby-checkbox" class="chkSubtitleLanguage" data-lang="${culture.ThreeLetterISOLanguageName.toLowerCase()}" /><span>${culture.DisplayName}</span></label>`; html += `<label><input type="checkbox" is="emby-checkbox" class="chkSubtitleLanguage" data-lang="${culture.ThreeLetterISOLanguageName.toLowerCase()}" /><span>${culture.DisplayName}</span></label>`;
} }
element.innerHTML = html; element.innerHTML = html;
@ -43,8 +41,7 @@ function populateCountries(select) {
return ApiClient.getCountries().then(allCountries => { return ApiClient.getCountries().then(allCountries => {
let html = ''; let html = '';
html += "<option value=''></option>"; html += "<option value=''></option>";
for (let i = 0; i < allCountries.length; i++) { for (const culture of allCountries) {
const culture = allCountries[i];
html += `<option value='${culture.TwoLetterISORegionName}'>${culture.DisplayName}</option>`; html += `<option value='${culture.TwoLetterISORegionName}'>${culture.DisplayName}</option>`;
} }
select.innerHTML = html; select.innerHTML = html;
@ -109,8 +106,7 @@ function renderMetadataSavers(page, metadataSavers) {
} }
html += `<h3 class="checkboxListLabel">${globalize.translate('LabelMetadataSavers')}</h3>`; html += `<h3 class="checkboxListLabel">${globalize.translate('LabelMetadataSavers')}</h3>`;
html += '<div class="checkboxList paperList checkboxList-paperList">'; html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0; i < metadataSavers.length; i++) { for (const plugin of metadataSavers) {
const plugin = metadataSavers[i];
html += `<label><input type="checkbox" data-defaultenabled="${plugin.DefaultEnabled}" is="emby-checkbox" class="chkMetadataSaver" data-pluginname="${escapeHtml(plugin.Name)}" ${false}><span>${escapeHtml(plugin.Name)}</span></label>`; html += `<label><input type="checkbox" data-defaultenabled="${plugin.DefaultEnabled}" is="emby-checkbox" class="chkMetadataSaver" data-pluginname="${escapeHtml(plugin.Name)}" ${false}><span>${escapeHtml(plugin.Name)}</span></label>`;
} }
html += '</div>'; html += '</div>';
@ -157,8 +153,7 @@ function getMetadataFetchersForTypeHtml(availableTypeOptions, libraryOptionsForT
function getTypeOptions(allOptions, type) { function getTypeOptions(allOptions, type) {
const allTypeOptions = allOptions.TypeOptions || []; const allTypeOptions = allOptions.TypeOptions || [];
for (let i = 0; i < allTypeOptions.length; i++) { for (const typeOptions of allTypeOptions) {
const typeOptions = allTypeOptions[i];
if (typeOptions.Type === type) return typeOptions; if (typeOptions.Type === type) return typeOptions;
} }
return null; return null;
@ -167,8 +162,7 @@ function getTypeOptions(allOptions, type) {
function renderMetadataFetchers(page, availableOptions, libraryOptions) { function renderMetadataFetchers(page, availableOptions, libraryOptions) {
let html = ''; let html = '';
const elem = page.querySelector('.metadataFetchers'); const elem = page.querySelector('.metadataFetchers');
for (let i = 0; i < availableOptions.TypeOptions.length; i++) { for (const availableTypeOptions of availableOptions.TypeOptions) {
const availableTypeOptions = availableOptions.TypeOptions[i];
html += getMetadataFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {}); html += getMetadataFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {});
} }
elem.innerHTML = html; elem.innerHTML = html;
@ -262,8 +256,7 @@ function getImageFetchersForTypeHtml(availableTypeOptions, libraryOptionsForType
function renderImageFetchers(page, availableOptions, libraryOptions) { function renderImageFetchers(page, availableOptions, libraryOptions) {
let html = ''; let html = '';
const elem = page.querySelector('.imageFetchers'); const elem = page.querySelector('.imageFetchers');
for (let i = 0; i < availableOptions.TypeOptions.length; i++) { for (const availableTypeOptions of availableOptions.TypeOptions) {
const availableTypeOptions = availableOptions.TypeOptions[i];
html += getImageFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {}); html += getImageFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {});
} }
elem.innerHTML = html; elem.innerHTML = html;
@ -460,8 +453,7 @@ function setSubtitleFetchersIntoOptions(parent, options) {
function setMetadataFetchersIntoOptions(parent, options) { function setMetadataFetchersIntoOptions(parent, options) {
const sections = parent.querySelectorAll('.metadataFetcher'); const sections = parent.querySelectorAll('.metadataFetcher');
for (let i = 0; i < sections.length; i++) { for (const section of sections) {
const section = sections[i];
const type = section.getAttribute('data-type'); const type = section.getAttribute('data-type');
let typeOptions = getTypeOptions(options, type); let typeOptions = getTypeOptions(options, type);
if (!typeOptions) { if (!typeOptions) {
@ -484,8 +476,7 @@ function setMetadataFetchersIntoOptions(parent, options) {
function setImageFetchersIntoOptions(parent, options) { function setImageFetchersIntoOptions(parent, options) {
const sections = parent.querySelectorAll('.imageFetcher'); const sections = parent.querySelectorAll('.imageFetcher');
for (let i = 0; i < sections.length; i++) { for (const section of sections) {
const section = sections[i];
const type = section.getAttribute('data-type'); const type = section.getAttribute('data-type');
let typeOptions = getTypeOptions(options, type); let typeOptions = getTypeOptions(options, type);
if (!typeOptions) { if (!typeOptions) {
@ -509,8 +500,7 @@ function setImageFetchersIntoOptions(parent, options) {
function setImageOptionsIntoOptions(options) { function setImageOptionsIntoOptions(options) {
const originalTypeOptions = (currentLibraryOptions || {}).TypeOptions || []; const originalTypeOptions = (currentLibraryOptions || {}).TypeOptions || [];
for (let i = 0; i < originalTypeOptions.length; i++) { for (const originalTypeOption of originalTypeOptions) {
const originalTypeOption = originalTypeOptions[i];
let typeOptions = getTypeOptions(options, originalTypeOption.Type); let typeOptions = getTypeOptions(options, originalTypeOption.Type);
if (!typeOptions) { if (!typeOptions) {

View file

@ -1,7 +1,7 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import datetime from '../../scripts/datetime'; import datetime from '../../scripts/datetime';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import itemHelper from '../itemHelper'; import itemHelper from '../itemHelper';
import indicators from '../indicators/indicators'; import indicators from '../indicators/indicators';
import 'material-design-icons-iconfont'; import 'material-design-icons-iconfont';

View file

@ -20,7 +20,7 @@ import '../../styles/flexstyles.scss';
import './style.scss'; import './style.scss';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import toast from '../toast/toast'; import toast from '../toast/toast';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import template from './metadataEditor.template.html'; import template from './metadataEditor.template.html';
let currentContext; let currentContext;
@ -955,8 +955,7 @@ function populatePeople(context, people) {
function getLockedFieldsHtml(fields, currentFields) { function getLockedFieldsHtml(fields, currentFields) {
let html = ''; let html = '';
for (let i = 0; i < fields.length; i++) { for (const field of fields) {
const field = fields[i];
const name = field.name; const name = field.name;
const value = field.value || field.name; const value = field.value || field.name;
const checkedHtml = currentFields.indexOf(value) === -1 ? ' checked' : ''; const checkedHtml = currentFields.indexOf(value) === -1 ? ' checked' : '';

View file

@ -564,3 +564,6 @@ export default function (options) {
}; };
} }
export const startMultiSelect = (card) => {
showSelections(card);
};

View file

@ -15,7 +15,7 @@ import appFooter from '../appFooter/appFooter';
import itemShortcuts from '../shortcuts'; import itemShortcuts from '../shortcuts';
import './nowPlayingBar.scss'; import './nowPlayingBar.scss';
import '../../elements/emby-slider/emby-slider'; import '../../elements/emby-slider/emby-slider';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
let currentPlayer; let currentPlayer;
let currentPlayerSupportedCommands = []; let currentPlayerSupportedCommands = [];

View file

@ -853,11 +853,9 @@ class PlaybackManager {
user: user user: user
}); });
for (let i = 0; i < responses.length; i++) { for (const subTargets of responses) {
const subTargets = responses[i]; for (const subTarget of subTargets) {
targets.push(subTarget);
for (let j = 0; j < subTargets.length; j++) {
targets.push(subTargets[j]);
} }
} }

View file

@ -4,7 +4,7 @@ import browser from '../../scripts/browser';
import loading from '../loading/loading'; import loading from '../loading/loading';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import { pluginManager } from '../pluginManager'; import { pluginManager } from '../pluginManager';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
import { enable, isEnabled, supported } from '../../scripts/autocast'; import { enable, isEnabled, supported } from '../../scripts/autocast';

View file

@ -1,4 +1,3 @@
import browser from '../../scripts/browser';
import appSettings from '../../scripts/settings/appSettings'; import appSettings from '../../scripts/settings/appSettings';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
import focusManager from '../focusManager'; 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) { function loadForm(context, user, userSettings, apiClient) {
const loggedInUserId = apiClient.getCurrentUserId(); const loggedInUserId = apiClient.getCurrentUserId();
const userId = user.Id; const userId = user.Id;
@ -209,8 +199,6 @@ function loadForm(context, user, userSettings, apiClient) {
fillSkipLengths(selectSkipBackLength); fillSkipLengths(selectSkipBackLength);
selectSkipBackLength.value = userSettings.skipBackLength(); selectSkipBackLength.value = userSettings.skipBackLength();
showOrHideEpisodesField(context);
loading.hide(); loading.hide();
} }

View file

@ -96,7 +96,7 @@
<div class="fieldDescription checkboxFieldDescription">${CinemaModeConfigurationHelp}</div> <div class="fieldDescription checkboxFieldDescription">${CinemaModeConfigurationHelp}</div>
</div> </div>
<div class="checkboxContainer fldEpisodeAutoPlay hide"> <div class="checkboxContainer fldEpisodeAutoPlay">
<label> <label>
<input type="checkbox" is="emby-checkbox" class="chkEpisodeAutoPlay" /> <input type="checkbox" is="emby-checkbox" class="chkEpisodeAutoPlay" />
<span>${PlayNextEpisodeAutomatically}</span> <span>${PlayNextEpisodeAutomatically}</span>

View file

@ -6,7 +6,7 @@ import layoutManager from '../layoutManager';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import { pluginManager } from '../pluginManager'; import { pluginManager } from '../pluginManager';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../scripts/settings/userSettings';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { PluginType } from '../../types/plugin.ts'; import { PluginType } from '../../types/plugin.ts';

View file

@ -4,7 +4,7 @@ import loading from './loading/loading';
import appSettings from '../scripts/settings/appSettings'; import appSettings from '../scripts/settings/appSettings';
import { playbackManager } from './playback/playbackmanager'; import { playbackManager } from './playback/playbackmanager';
import { appHost } from '../components/apphost'; import { appHost } from '../components/apphost';
import { appRouter } from '../components/appRouter'; import { appRouter } from './router/appRouter';
import * as inputManager from '../scripts/inputManager'; import * as inputManager from '../scripts/inputManager';
import toast from '../components/toast/toast'; import toast from '../components/toast/toast';
import confirm from '../components/confirm/confirm'; import confirm from '../components/confirm/confirm';

View file

@ -18,7 +18,7 @@ import './remotecontrol.scss';
import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-ratingbutton/emby-ratingbutton';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import toast from '../toast/toast'; import toast from '../toast/toast';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
let showMuteButton = true; let showMuteButton = true;
let showVolumeSlider = true; let showVolumeSlider = true;

View file

@ -0,0 +1,44 @@
import loadable from '@loadable/component';
import React from 'react';
import { Route } from 'react-router-dom';
export enum AsyncRouteType {
Stable,
Experimental
}
export interface AsyncRoute {
/** The URL path for this route. */
path: string
/** The relative path to the page component in the routes directory. */
page: string
/** The route should use the page component from the experimental app. */
type?: AsyncRouteType
}
interface AsyncPageProps {
/** The relative path to the page component in the routes directory. */
page: string
}
const ExperimentalAsyncPage = loadable(
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/experimental/routes/${props.page}`),
{ cacheKey: (props: AsyncPageProps) => props.page }
);
const StableAsyncPage = loadable(
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/stable/routes/${props.page}`),
{ cacheKey: (props: AsyncPageProps) => props.page }
);
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
<Route
key={path}
path={path}
element={(
type === AsyncRouteType.Experimental ?
<ExperimentalAsyncPage page={page} /> :
<StableAsyncPage page={page} />
)}
/>
);

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom'; import { Route } from 'react-router-dom';
import ViewManagerPage, { ViewManagerPageProps } from '../components/viewManager/ViewManagerPage'; import ViewManagerPage, { ViewManagerPageProps } from '../viewManager/ViewManagerPage';
export interface LegacyRoute { export interface LegacyRoute {
path: string, path: string,

View file

@ -1,15 +1,15 @@
import { Action, createHashHistory } from 'history'; import { Action, createHashHistory } from 'history';
import { appHost } from './apphost'; import { appHost } from '../apphost';
import { clearBackdrop, setBackdropTransparency } from './backdrop/backdrop'; import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
import globalize from '../scripts/globalize'; import globalize from '../../scripts/globalize';
import Events from '../utils/events.ts'; import Events from '../../utils/events.ts';
import itemHelper from './itemHelper'; import itemHelper from '../itemHelper';
import loading from './loading/loading'; import loading from '../loading/loading';
import viewManager from './viewManager/viewManager'; import viewManager from '../viewManager/viewManager';
import ServerConnections from './ServerConnections'; import ServerConnections from '../ServerConnections';
import alert from './alert'; import alert from '../alert';
import { ConnectionState } from '../utils/jellyfin-apiclient/ConnectionState.ts'; import { ConnectionState } from '../../utils/jellyfin-apiclient/ConnectionState.ts';
export const history = createHashHistory(); export const history = createHashHistory();
@ -247,7 +247,7 @@ class AppRouter {
url = apiClient.getUrl(`/web${url}`); url = apiClient.getUrl(`/web${url}`);
promise = apiClient.get(url); promise = apiClient.get(url);
} else { } else {
promise = import(/* webpackChunkName: "[request]" */ `../controllers/${url}`); promise = import(/* webpackChunkName: "[request]" */ `../../controllers/${url}`);
} }
promise.then((html) => { promise.then((html) => {
@ -267,7 +267,7 @@ class AppRouter {
}; };
if (route.controller) { if (route.controller) {
import(/* webpackChunkName: "[request]" */ '../controllers/' + route.controller).then(onInitComplete); import(/* webpackChunkName: "[request]" */ '../../controllers/' + route.controller).then(onInitComplete);
} else { } else {
onInitComplete(); onInitComplete();
} }

View file

@ -39,9 +39,9 @@ try {
const elem = document.createElement('div'); const elem = document.createElement('div');
const opts = Object.defineProperty({}, 'behavior', { const opts = Object.defineProperty({}, 'behavior', {
// eslint-disable-next-line getter-return
get: function () { get: function () {
supportsScrollToOptions = true; supportsScrollToOptions = true;
return null;
} }
}); });

View file

@ -24,7 +24,7 @@ type LiveTVSearchResultsProps = {
parentId?: string | null; parentId?: string | null;
collectionType?: string | null; collectionType?: string | null;
query?: string; query?: string;
} };
/* /*
* React component to display search result rows for live tv library search * React component to display search result rows for live tv library search
@ -79,7 +79,9 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
IsMovie: true IsMovie: true
}).then(result => setMovies(result.Items || [])); })
.then(result => setMovies(result.Items || []))
.catch(() => setMovies([]));
// Episodes row // Episodes row
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
@ -88,22 +90,30 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false, IsSports: false,
IsKids: false, IsKids: false,
IsNews: false IsNews: false
}).then(result => setEpisodes(result.Items || [])); })
.then(result => setEpisodes(result.Items || []))
.catch(() => setEpisodes([]));
// Sports row // Sports row
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
IsSports: true IsSports: true
}).then(result => setSports(result.Items || [])); })
.then(result => setSports(result.Items || []))
.catch(() => setSports([]));
// Kids row // Kids row
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
IsKids: true IsKids: true
}).then(result => setKids(result.Items || [])); })
.then(result => setKids(result.Items || []))
.catch(() => setKids([]));
// News row // News row
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
IsNews: true IsNews: true
}).then(result => setNews(result.Items || [])); })
.then(result => setNews(result.Items || []))
.catch(() => setNews([]));
// Programs row // Programs row
fetchItems(apiClient, { fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram', IncludeItemTypes: 'LiveTvProgram',
@ -112,10 +122,13 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false, IsSports: false,
IsKids: false, IsKids: false,
IsNews: false IsNews: false
}).then(result => setPrograms(result.Items || [])); })
.then(result => setPrograms(result.Items || []))
.catch(() => setPrograms([]));
// Channels row // Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items || [])); .then(result => setChannels(result.Items || []))
.catch(() => setChannels([]));
} }
}, [collectionType, parentId, query, serverId]); }, [collectionType, parentId, query, serverId]);

View file

@ -1,7 +1,7 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import type { ApiClient } from 'jellyfin-apiclient'; import type { ApiClient } from 'jellyfin-apiclient';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
@ -12,7 +12,18 @@ type SearchResultsProps = {
parentId?: string | null; parentId?: string | null;
collectionType?: string | null; collectionType?: string | null;
query?: string; query?: string;
} };
const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({
...result,
Items: result.Items || []
});
const isMovies = (collectionType: string) => collectionType === 'movies';
const isMusic = (collectionType: string) => collectionType === 'music';
const isTVShows = (collectionType: string) => collectionType === 'tvshows' || collectionType === 'tv';
/* /*
* React component to display search result rows for global search and non-live tv library search * React component to display search result rows for global search and non-live tv library search
@ -35,55 +46,55 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const [ people, setPeople ] = useState<BaseItemDto[]>([]); const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]); const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
useEffect(() => { const getDefaultParameters = useCallback(() => ({
const getDefaultParameters = () => ({ ParentId: parentId,
ParentId: parentId, searchTerm: query,
searchTerm: query, Limit: 24,
Limit: 24, Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', Recursive: true,
Recursive: true, EnableTotalRecordCount: false,
EnableTotalRecordCount: false, ImageTypeLimit: 1,
ImageTypeLimit: 1, IncludePeople: false,
IncludePeople: false, IncludeMedia: false,
IncludeMedia: false, IncludeGenres: false,
IncludeGenres: false, IncludeStudios: false,
IncludeStudios: false, IncludeArtists: false
IncludeArtists: false }), [parentId, query]);
});
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists( const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getArtists(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludeArtists: true, IncludeArtists: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems( const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getItems(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludeMedia: true, IncludeMedia: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople( const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getPeople(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludePeople: true, IncludePeople: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const isMovies = () => collectionType === 'movies';
const isMusic = () => collectionType === 'music';
const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv';
useEffect(() => {
// Reset state // Reset state
setMovies([]); setMovies([]);
setShows([]); setShows([]);
@ -102,78 +113,99 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
setPeople([]); setPeople([]);
setCollections([]); setCollections([]);
if (query) { if (!query) {
const apiClient = ServerConnections.getApiClient(serverId); return;
// Movie libraries
if (!collectionType || isMovies()) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items || []));
}
// TV Show libraries
if (!collectionType || isTVShows()) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items || []));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items || []));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies() || isTVShows()) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
}
// Music libraries
if (!collectionType || isMusic()) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items || []));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items || []));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items || []));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items || []));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items || []));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(results => setPhotoAlbums(results.Items || []));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(results => setPhotos(results.Items || []));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(results => setAudioBooks(results.Items || []));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items || []));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items || []));
}
} }
}, [collectionType, parentId, query, serverId]);
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies(collectionType)) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items))
.catch(() => setMovies([]));
}
// TV Show libraries
if (!collectionType || isTVShows(collectionType)) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items))
.catch(() => setShows([]));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items))
.catch(() => setEpisodes([]));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) {
// People row
fetchPeople(apiClient)
.then(result => setPeople(result.Items))
.catch(() => setPeople([]));
}
// Music libraries
if (!collectionType || isMusic(collectionType)) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items))
.catch(() => setPlaylists([]));
// Artists row
fetchArtists(apiClient)
.then(result => setArtists(result.Items))
.catch(() => setArtists([]));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items))
.catch(() => setAlbums([]));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items))
.catch(() => setSongs([]));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
})
.then(result => setVideos(result.Items))
.catch(() => setVideos([]));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items))
.catch(() => setPrograms([]));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items))
.catch(() => setChannels([]));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(result => setPhotoAlbums(result.Items))
.catch(() => setPhotoAlbums([]));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(result => setPhotos(result.Items))
.catch(() => setPhotos([]));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(result => setAudioBooks(result.Items))
.catch(() => setAudioBooks([]));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(result => setBooks(result.Items))
.catch(() => setBooks([]));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items))
.catch(() => setCollections([]));
}
}, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]);
return ( return (
<div <div

View file

@ -35,13 +35,13 @@ type CardOptions = {
showChannelName?: boolean, showChannelName?: boolean,
showTitle?: boolean, showTitle?: boolean,
showYear?: boolean showYear?: boolean
} };
type SearchResultsRowProps = { type SearchResultsRowProps = {
title?: string; title?: string;
items?: BaseItemDto[]; items?: BaseItemDto[];
cardOptions?: CardOptions; cardOptions?: CardOptions;
} };
const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => { const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);

View file

@ -5,7 +5,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useEffect, useState } from 'react';
import { appRouter } from '../appRouter'; import { appRouter } from '../router/appRouter';
import { useApi } from '../../hooks/useApi'; import { useApi } from '../../hooks/useApi';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
@ -25,7 +25,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
type SearchSuggestionsProps = { type SearchSuggestionsProps = {
parentId?: string | null; parentId?: string | null;
} };
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }: SearchSuggestionsProps) => { const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]); const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
@ -45,7 +45,11 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId
parentId: parentId || undefined, parentId: parentId || undefined,
enableTotalRecordCount: false 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 ]); }, [ api, parentId, user ]);

View file

@ -5,7 +5,7 @@
import { playbackManager } from './playback/playbackmanager'; import { playbackManager } from './playback/playbackmanager';
import inputManager from '../scripts/inputManager'; import inputManager from '../scripts/inputManager';
import { appRouter } from './appRouter'; import { appRouter } from './router/appRouter';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import dom from '../scripts/dom'; import dom from '../scripts/dom';
import recordingHelper from './recordingcreator/recordinghelper'; import recordingHelper from './recordingcreator/recordinghelper';

View file

@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import type { RestoreViewFailResponse } from '../../types/viewManager';
import viewManager from './viewManager'; import viewManager from './viewManager';
export interface ViewManagerPageProps { export interface ViewManagerPageProps {
@ -45,7 +46,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
}; };
viewManager.tryRestoreView(viewOptions) viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => { .catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) { if (!result || !result.cancelled) {
const [ controllerFactory, viewHtml ] = await Promise.all([ const [ controllerFactory, viewHtml ] = await Promise.all([
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`), import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
@ -63,7 +64,10 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
}; };
loadPage(); 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, controller,
view, view,
type, type,
@ -73,8 +77,6 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
transition, transition,
location.pathname, location.pathname,
location.search location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]); ]);
return <></>; return <></>;

View file

@ -90,11 +90,12 @@ function load(page, devices) {
let html = ''; let html = '';
html += devices.map(function (device) { html += devices.map(function (device) {
let deviceHtml = ''; let deviceHtml = '';
deviceHtml += "<div data-id='" + device.Id + "' class='card backdropCard'>"; deviceHtml += "<div data-id='" + escapeHtml(device.Id) + "' class='card backdropCard'>";
deviceHtml += '<div class="cardBox visualCardBox">'; deviceHtml += '<div class="cardBox visualCardBox">';
deviceHtml += '<div class="cardScalable">'; deviceHtml += '<div class="cardScalable">';
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>'; deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
deviceHtml += `<a is="emby-linkbutton" href="#/device.html?id=${device.Id}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`; deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
// audit note: getDeviceIcon returns static text
const iconUrl = imageHelper.getDeviceIcon(device); const iconUrl = imageHelper.getDeviceIcon(device);
if (iconUrl) { if (iconUrl) {
@ -113,7 +114,7 @@ function load(page, devices) {
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">'; deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
else else
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">'; deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + device.Id + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>'; deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
deviceHtml += '</div>'; deviceHtml += '</div>';
} }

View file

@ -167,6 +167,14 @@
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a> <a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a>
</div> </div>
</div> </div>
<div class="selectContainer">
<select is="emby-select" id="selectTonemappingMode" label="${LabelTonemappingMode}">
<option value="auto">${Auto}</option>
<option value="max">MAX</option>
<option value="rgb">RGB</option>
</select>
<div class="fieldDescription">${TonemappingModeHelp}</div>
</div>
<div class="selectContainer"> <div class="selectContainer">
<select is="emby-select" id="selectTonemappingRange" label="${LabelTonemappingRange}"> <select is="emby-select" id="selectTonemappingRange" label="${LabelTonemappingRange}">
<option value="auto">${Auto}</option> <option value="auto">${Auto}</option>
@ -179,10 +187,6 @@
<input is="emby-input" type="number" id="txtTonemappingDesat" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingDesat}" /> <input is="emby-input" type="number" id="txtTonemappingDesat" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingDesat}" />
<div class="fieldDescription">${LabelTonemappingDesatHelp}</div> <div class="fieldDescription">${LabelTonemappingDesatHelp}</div>
</div> </div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtTonemappingThreshold" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingThreshold}" />
<div class="fieldDescription">${LabelTonemappingThresholdHelp}</div>
</div>
<div class="inputContainer"> <div class="inputContainer">
<input is="emby-input" type="number" id="txtTonemappingPeak" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingPeak}" /> <input is="emby-input" type="number" id="txtTonemappingPeak" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingPeak}" />
<div class="fieldDescription">${LabelTonemappingPeakHelp}</div> <div class="fieldDescription">${LabelTonemappingPeakHelp}</div>

View file

@ -32,9 +32,9 @@ function loadPage(page, config, systemInfo) {
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping; page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping; page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm; page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange; page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat; page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold;
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak; page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || ''; page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness; page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
@ -90,9 +90,9 @@ function onSubmit() {
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked; config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked; config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value; config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value; config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value; config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value;
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value; config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0'; config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value; config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;

View file

@ -28,8 +28,7 @@ function populateVersions(packageInfo, page, installedPlugin) {
return b.timestamp < a.timestamp ? -1 : 1; return b.timestamp < a.timestamp ? -1 : 1;
}); });
for (let i = 0; i < packageInfo.versions.length; i++) { for (const version of packageInfo.versions) {
const version = packageInfo.versions[i];
html += '<option value="' + version.version + '">' + globalize.translate('PluginFromRepo', version.version, version.repositoryName) + '</option>'; html += '<option value="' + version.version + '">' + globalize.translate('PluginFromRepo', version.version, version.repositoryName) + '</option>';
} }

View file

@ -66,8 +66,7 @@ function populateList(options) {
let currentCategory = null; let currentCategory = null;
let html = ''; let html = '';
for (let i = 0; i < availablePlugins.length; i++) { for (const plugin of availablePlugins) {
const plugin = availablePlugins[i];
const category = plugin.categoryDisplayName; const category = plugin.categoryDisplayName;
if (category != currentCategory) { if (category != currentCategory) {
if (currentCategory) { if (currentCategory) {

Some files were not shown because too many files have changed in this diff Show more