diff --git a/.eslintrc.js b/.eslintrc.js index 44fd0a3046..7b94a83ed3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], 'comma-dangle': ['error', 'never'], 'comma-spacing': ['error'], + 'curly': ['error', 'multi-line', 'consistent'], 'default-case-last': ['error'], 'eol-last': ['error'], 'indent': ['error', 4, { 'SwitchCase': 1 }], @@ -48,6 +49,7 @@ module.exports = { 'no-empty-function': ['error'], 'no-extend-native': ['error'], 'no-floating-decimal': ['error'], + 'no-lonely-if': ['error'], 'no-multi-spaces': ['error'], 'no-multiple-empty-lines': ['error', { 'max': 1 }], 'no-nested-ternary': ['error'], @@ -63,6 +65,7 @@ module.exports = { 'no-trailing-spaces': ['error'], 'no-unused-expressions': ['off'], '@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], + 'no-unused-private-class-members': ['error'], 'no-useless-constructor': ['off'], '@typescript-eslint/no-useless-constructor': ['error'], 'no-var': ['error'], diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 52006a7b46..eccfd4cf27 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,6 +65,7 @@ - [Merlin Sievers](https://github.com/dann-merlin) - [Fishbigger](https://github.com/fishbigger) - [sleepycatcoding](https://github.com/sleepycatcoding) + - [TheMelmacian](https://github.com/TheMelmacian) # Emby Contributors diff --git a/package-lock.json b/package-lock.json index 02e521e4fd..2ecbd9d155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "0.19.0", "expose-loader": "4.1.0", + "fork-ts-checker-webpack-plugin": "8.0.0", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.3", "mini-css-extract-plugin": "2.7.6", @@ -106,6 +107,7 @@ "sass": "1.62.1", "sass-loader": "13.3.2", "source-map-loader": "4.0.1", + "speed-measure-webpack-plugin": "1.5.0", "style-loader": "3.3.3", "stylelint": "15.6.2", "stylelint-config-rational-order": "0.1.2", @@ -115,6 +117,7 @@ "ts-loader": "9.4.4", "typescript": "5.0.4", "webpack": "5.88.1", + "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", "webpack-merge": "5.9.0", @@ -2564,9 +2567,9 @@ } }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -3397,6 +3400,12 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "dev": true + }, "node_modules/@popperjs/core": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", @@ -4497,6 +4506,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -6980,6 +6998,12 @@ "node": ">=8" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8852,6 +8876,241 @@ "node": ">=0.10.0" } }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -9259,6 +9518,21 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -11024,6 +11298,24 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.invokemap": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz", + "integrity": "sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11036,6 +11328,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.pullall": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz", + "integrity": "sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==", + "dev": true + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11054,6 +11352,12 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "dev": true }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, "node_modules/logform": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", @@ -11578,6 +11882,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11677,6 +11990,12 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -12102,6 +12421,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -15322,6 +15650,20 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true }, + "node_modules/sirv": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", + "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -15724,6 +16066,91 @@ "specificity": "bin/specificity" } }, + "node_modules/speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/speed-measure-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -18769,9 +19196,9 @@ } }, "node_modules/tapable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", - "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, "engines": { "node": ">=6" @@ -19044,6 +19471,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -19763,6 +20199,79 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz", + "integrity": "sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "is-plain-object": "^5.0.0", + "lodash.debounce": "^4.0.8", + "lodash.escape": "^4.0.1", + "lodash.flatten": "^4.4.0", + "lodash.invokemap": "^4.6.0", + "lodash.pullall": "^4.2.0", + "lodash.uniqby": "^4.7.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", @@ -22165,9 +22674,9 @@ } }, "@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, "@emotion/babel-plugin": { @@ -22731,6 +23240,12 @@ "fastq": "^1.6.0" } }, + "@polka/url": { + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "dev": true + }, "@popperjs/core": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", @@ -23600,6 +24115,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -25439,6 +25960,12 @@ "is-obj": "^2.0.0" } }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -26879,6 +27406,174 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -27188,6 +27883,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -28491,6 +29195,24 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.invokemap": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz", + "integrity": "sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -28503,6 +29225,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.pullall": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz", + "integrity": "sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -28521,6 +29249,12 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "dev": true }, + "lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, "logform": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", @@ -28905,6 +29639,12 @@ "minimist": "^1.2.5" } }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -28986,6 +29726,12 @@ "tslib": "^2.0.3" } }, + "node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true + }, "node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -29310,6 +30056,12 @@ "is-wsl": "^2.2.0" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -31565,6 +32317,17 @@ } } }, + "sirv": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", + "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -31901,6 +32664,66 @@ "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", "dev": true }, + "speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "requires": { + "chalk": "^4.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -34271,9 +35094,9 @@ } }, "tapable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", - "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, "tar": { @@ -34471,6 +35294,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + }, "tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -35060,6 +35889,52 @@ } } }, + "webpack-bundle-analyzer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz", + "integrity": "sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "is-plain-object": "^5.0.0", + "lodash.debounce": "^4.0.8", + "lodash.escape": "^4.0.1", + "lodash.flatten": "^4.4.0", + "lodash.invokemap": "^4.6.0", + "lodash.pullall": "^4.2.0", + "lodash.uniqby": "^4.7.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "requires": {} + } + } + }, "webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", diff --git a/package.json b/package.json index 5c9fd988a9..988c2a0215 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "0.19.0", "expose-loader": "4.1.0", + "fork-ts-checker-webpack-plugin": "8.0.0", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.3", "mini-css-extract-plugin": "2.7.6", @@ -48,6 +49,7 @@ "sass": "1.62.1", "sass-loader": "13.3.2", "source-map-loader": "4.0.1", + "speed-measure-webpack-plugin": "1.5.0", "style-loader": "3.3.3", "stylelint": "15.6.2", "stylelint-config-rational-order": "0.1.2", @@ -57,6 +59,7 @@ "ts-loader": "9.4.4", "typescript": "5.0.4", "webpack": "5.88.1", + "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", "webpack-merge": "5.9.0", @@ -136,6 +139,7 @@ "scripts": { "start": "npm run serve", "serve": "webpack serve --config webpack.dev.js", + "build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js", "build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js", "build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js", "build:check": "tsc --noEmit", diff --git a/src/apps/experimental/components/library/GridListViewButton.tsx b/src/apps/experimental/components/library/GridListViewButton.tsx new file mode 100644 index 0000000000..26588b1883 --- /dev/null +++ b/src/apps/experimental/components/library/GridListViewButton.tsx @@ -0,0 +1,62 @@ +import React, { FC, useCallback } from 'react'; +import { ButtonGroup, IconButton } from '@mui/material'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import ViewListIcon from '@mui/icons-material/ViewList'; +import globalize from 'scripts/globalize'; +import { LibraryViewSettings, ViewMode } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import ViewSettingsButton from './ViewSettingsButton'; + +interface GridListViewButtonProps { + viewType: LibraryTab; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; +} + +const GridListViewButton: FC = ({ + viewType, + libraryViewSettings, + setLibraryViewSettings +}) => { + const handleToggleCurrentView = useCallback(() => { + setLibraryViewSettings((prevState) => ({ + ...prevState, + ViewMode: + prevState.ViewMode === ViewMode.ListView ? ViewMode.GridView : ViewMode.ListView + })); + }, [setLibraryViewSettings]); + + const isGridView = libraryViewSettings.ViewMode === ViewMode.GridView; + + return ( + + {isGridView ? ( + + ) : ( + + + + )} + + + + + + ); +}; + +export default GridListViewButton; diff --git a/src/apps/experimental/components/library/ViewSettingsButton.tsx b/src/apps/experimental/components/library/ViewSettingsButton.tsx new file mode 100644 index 0000000000..cec5090acc --- /dev/null +++ b/src/apps/experimental/components/library/ViewSettingsButton.tsx @@ -0,0 +1,198 @@ +import { ImageType } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback } from 'react'; + +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Checkbox from '@mui/material/Checkbox'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import Box from '@mui/material/Box'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import Popover from '@mui/material/Popover'; +import ViewComfyIcon from '@mui/icons-material/ViewComfy'; + +import globalize from 'scripts/globalize'; +import { LibraryViewSettings, ViewMode } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +const imageTypesOptions = [ + { label: 'Primary', value: ImageType.Primary }, + { label: 'Banner', value: ImageType.Banner }, + { label: 'Disc', value: ImageType.Disc }, + { label: 'Logo', value: ImageType.Logo }, + { label: 'Thumb', value: ImageType.Thumb } +]; + +interface ViewSettingsButtonProps { + viewType: LibraryTab; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; +} + +const ViewSettingsButton: FC = ({ + viewType, + libraryViewSettings, + setLibraryViewSettings +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const id = open ? 'selectview-popover' : undefined; + + const handleClick = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const name = event.target.name; + + setLibraryViewSettings((prevState) => ({ + ...prevState, + [name]: event.target.checked + })); + }, + [setLibraryViewSettings] + ); + + const onSelectChange = useCallback( + (event: SelectChangeEvent) => { + setLibraryViewSettings((prevState) => ({ + ...prevState, + ImageType: event.target.value as ImageType + })); + }, + [setLibraryViewSettings] + ); + + const getVisibleImageType = () => { + const visibleImageType: ImageType[] = [ImageType.Primary]; + + if ( + viewType !== LibraryTab.Episodes + && viewType !== LibraryTab.Artists + && viewType !== LibraryTab.AlbumArtists + && viewType !== LibraryTab.Albums + ) { + visibleImageType.push(ImageType.Banner); + visibleImageType.push(ImageType.Disc); + visibleImageType.push(ImageType.Logo); + visibleImageType.push(ImageType.Thumb); + } + + return visibleImageType; + }; + + const isViewSettingsEnabled = () => { + return libraryViewSettings.ViewMode !== ViewMode.ListView; + }; + + return ( + + + + + + + + + {globalize.translate('LabelImageType')} + + + + + {isViewSettingsEnabled() && ( + <> + + + + + } + label={globalize.translate('ShowTitle')} + /> + + } + label={globalize.translate('ShowYear')} + /> + + } + label={globalize.translate( + 'EnableCardLayout' + )} + /> + + + + )} + + + ); +}; + +export default ViewSettingsButton; diff --git a/src/components/alert.js b/src/components/alert.js index 6e654e9f3c..ebeed3d8fd 100644 --- a/src/components/alert.js +++ b/src/components/alert.js @@ -3,41 +3,32 @@ import browser from '../scripts/browser'; import dialog from './dialog/dialog'; import globalize from '../scripts/globalize'; -function useNativeAlert() { - // webOS seems to block modals - // Tizen 2.x seems to block modals - return !browser.web0s - && !(browser.tizenVersion && browser.tizenVersion < 3) - && browser.tv - && window.alert; -} - export default async function (text, title) { - let options; - if (typeof text === 'string') { - options = { - title: title, - text: text - }; - } else { - options = text; - } + // Modals seem to be blocked on Web OS and Tizen 2.x + const canUseNativeAlert = !!( + !browser.web0s + && !(browser.tizenVersion && browser.tizenVersion < 3) + && browser.tv + && window.alert + ); + + const options = typeof text === 'string' ? { title, text } : text; await appRouter.ready(); - if (useNativeAlert()) { + if (canUseNativeAlert) { alert((options.text || '').replaceAll('
', '\n')); - return Promise.resolve(); - } else { - const items = []; - items.push({ + return Promise.resolve(); + } + + options.buttons = [ + { name: globalize.translate('ButtonGotIt'), id: 'ok', type: 'submit' - }); + } + ]; - options.buttons = items; - return dialog.show(options); - } + return dialog.show(options); } diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index d5b19c0210..96d7edb06a 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -803,19 +803,17 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, } else { lines.push(escapeHtml(item.SeriesName)); } + } else if (isUsingLiveTvNaming(item)) { + lines.push(escapeHtml(item.Name)); + + if (!item.EpisodeTitle && !item.IndexNumber) { + titleAdded = true; + } } else { - if (isUsingLiveTvNaming(item)) { - lines.push(escapeHtml(item.Name)); + const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''; - if (!item.EpisodeTitle && !item.IndexNumber) { - titleAdded = true; - } - } else { - const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''; - - if (parentTitle || showTitle) { - lines.push(escapeHtml(parentTitle)); - } + if (parentTitle || showTitle) { + lines.push(escapeHtml(parentTitle)); } } } @@ -898,13 +896,11 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, if (item.Type === 'Series') { if (item.Status === 'Continuing') { lines.push(globalize.translate('SeriesYearToPresent', productionYear || '')); + } else if (item.EndDate && item.ProductionYear) { + const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false }); + lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); } else { - if (item.EndDate && item.ProductionYear) { - const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false }); - lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); - } else { - lines.push(productionYear || ''); - } + lines.push(productionYear || ''); } } else { lines.push(productionYear || ''); diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js index 9384ba2056..76c54693f9 100644 --- a/src/components/guide/guide.js +++ b/src/components/guide/guide.js @@ -762,12 +762,10 @@ function Guide(options) { } else { container.scrollTo(0, pos); } + } else if (horizontal) { + container.scrollLeft = Math.round(pos); } else { - if (horizontal) { - container.scrollLeft = Math.round(pos); - } else { - container.scrollTop = Math.round(pos); - } + container.scrollTop = Math.round(pos); } } diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 2bb9bb7928..1b26f8624a 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -216,14 +216,12 @@ function getFetchLatestItemsFn(serverId, parentId, collectionType) { if (collectionType === 'music') { limit = 30; } + } else if (collectionType === 'tvshows') { + limit = 5; + } else if (collectionType === 'music') { + limit = 9; } else { - if (collectionType === 'tvshows') { - limit = 5; - } else if (collectionType === 'music') { - limit = 9; - } else { - limit = 8; - } + limit = 8; } const options = { diff --git a/src/components/htmlMediaHelper.js b/src/components/htmlMediaHelper.js index 5db0428cda..c63eb29db0 100644 --- a/src/components/htmlMediaHelper.js +++ b/src/components/htmlMediaHelper.js @@ -76,20 +76,18 @@ export function handleHlsJsMediaError(instance, reject) { recoverDecodingErrorDate = now; console.debug('try to recover media Error ...'); hlsPlayer.recoverMediaError(); + } else if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) { + recoverSwapAudioCodecDate = now; + console.debug('try to swap Audio Codec and recover media Error ...'); + hlsPlayer.swapAudioCodec(); + hlsPlayer.recoverMediaError(); } else { - if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) { - recoverSwapAudioCodecDate = now; - console.debug('try to swap Audio Codec and recover media Error ...'); - hlsPlayer.swapAudioCodec(); - hlsPlayer.recoverMediaError(); - } else { - console.error('cannot recover, last media error recovery failed ...'); + console.error('cannot recover, last media error recovery failed ...'); - if (reject) { - reject(); - } else { - onErrorInternal(instance, 'mediadecodeerror'); - } + if (reject) { + reject(); + } else { + onErrorInternal(instance, 'mediadecodeerror'); } } } diff --git a/src/components/imageDownloader/imageDownloader.js b/src/components/imageDownloader/imageDownloader.js index 1955896180..380803d664 100644 --- a/src/components/imageDownloader/imageDownloader.js +++ b/src/components/imageDownloader/imageDownloader.js @@ -171,14 +171,12 @@ function getRemoteImageHtml(image, imageType) { shape = 'banner'; } else if (imageType === 'Disc') { shape = 'square'; + } else if (currentItemType === 'Episode') { + shape = 'backdrop'; + } else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') { + shape = 'square'; } else { - if (currentItemType === 'Episode') { - shape = 'backdrop'; - } else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') { - shape = 'square'; - } else { - shape = 'portrait'; - } + shape = 'portrait'; } cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable'; @@ -230,10 +228,8 @@ function getRemoteImageHtml(image, imageType) { if (image.Language) { html += ' • ' + image.Language; } - } else { - if (image.Language) { - html += image.Language; - } + } else if (image.Language) { + html += image.Language; } html += ''; @@ -244,16 +240,14 @@ function getRemoteImageHtml(image, imageType) { if (image.RatingType === 'Likes') { html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes'); - } else { - if (image.CommunityRating) { - html += image.CommunityRating.toFixed(1); + } else if (image.CommunityRating) { + html += image.CommunityRating.toFixed(1); - if (image.VoteCount) { - html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes'); - } - } else { - html += 'Unrated'; + if (image.VoteCount) { + html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes'); } + } else { + html += 'Unrated'; } html += ''; diff --git a/src/components/imageeditor/imageeditor.js b/src/components/imageeditor/imageeditor.js index 6a7982cca9..9e7a9d5710 100644 --- a/src/components/imageeditor/imageeditor.js +++ b/src/components/imageeditor/imageeditor.js @@ -164,10 +164,8 @@ function getCardHtml(image, apiClient, options) { } else { html += ''; } - } else { - if (options.imageProviders.length) { - html += ''; - } + } else if (options.imageProviders.length) { + html += ''; } html += ''; diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index d35bdc3527..1a230c31ed 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -374,14 +374,12 @@ export function getListViewHtml(options) { if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') { textlines.push(item.AlbumArtist); } - } else { - if (options.artist) { - const artistItems = item.ArtistItems; - if (artistItems && item.Type !== 'MusicAlbum') { - textlines.push(artistItems.map(a => { - return a.Name; - }).join(', ')); - } + } else if (options.artist) { + const artistItems = item.ArtistItems; + if (artistItems && item.Type !== 'MusicAlbum') { + textlines.push(artistItems.map(a => { + return a.Name; + }).join(', ')); } } diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 37400559f2..fec43db3e8 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -649,10 +649,8 @@ function onPlaybackStopped(e, state) { if (state.NextMediaType !== 'Audio') { hideNowPlayingBar(); } - } else { - if (!state.NextMediaType) { - hideNowPlayingBar(); - } + } else if (!state.NextMediaType) { + hideNowPlayingBar(); } } diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 308acd79f5..05117fd08d 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -14,6 +14,7 @@ import alert from '../alert'; import { PluginType } from '../../types/plugin.ts'; import { includesAny } from '../../utils/container.ts'; import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts'; +import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; const UNLIMITED_ITEMS = -1; @@ -154,28 +155,6 @@ function mergePlaybackQueries(obj1, obj2) { return query; } -function backdropImageUrl(apiClient, item, options) { - options = options || {}; - options.type = options.type || 'Backdrop'; - - // If not resizing, get the original image - if (!options.maxWidth && !options.width && !options.maxHeight && !options.height && !options.fillWidth && !options.fillHeight) { - options.quality = 100; - } - - if (item.BackdropImageTags?.length) { - options.tag = item.BackdropImageTags[0]; - return apiClient.getScaledImageUrl(item.Id, options); - } - - if (item.ParentBackdropImageTags?.length) { - options.tag = item.ParentBackdropImageTags[0]; - return apiClient.getScaledImageUrl(item.ParentBackdropItemId, options); - } - - return null; -} - function getMimeType(type, container) { container = (container || '').toLowerCase(); @@ -1437,15 +1416,13 @@ class PlaybackManager { if (Screenfull.isEnabled) { Screenfull.toggle(); - } else { + } else if (document.webkitIsFullScreen && document.webkitCancelFullscreen) { // iOS Safari - if (document.webkitIsFullScreen && document.webkitCancelFullscreen) { - document.webkitCancelFullscreen(); - } else { - const elem = document.querySelector('video'); - if (elem?.webkitEnterFullscreen) { - elem.webkitEnterFullscreen(); - } + document.webkitCancelFullscreen(); + } else { + const elem = document.querySelector('video'); + if (elem?.webkitEnterFullscreen) { + elem.webkitEnterFullscreen(); } } }; @@ -2345,30 +2322,23 @@ class PlaybackManager { let prevRelIndex = 0; for (const stream of prevSource.MediaStreams) { - if (stream.Type != streamType) - continue; + if (stream.Type != streamType) continue; - if (stream.Index == prevIndex) - break; + if (stream.Index == prevIndex) break; prevRelIndex += 1; } let newRelIndex = 0; for (const stream of mediaSource.MediaStreams) { - if (stream.Type != streamType) - continue; + if (stream.Type != streamType) continue; let score = 0; - if (prevStream.Codec == stream.Codec) - score += 1; - if (prevRelIndex == newRelIndex) - score += 1; - if (prevStream.DisplayTitle && prevStream.DisplayTitle == stream.DisplayTitle) - score += 2; - if (prevStream.Language && prevStream.Language != 'und' && prevStream.Language == stream.Language) - score += 2; + if (prevStream.Codec == stream.Codec) score += 1; + if (prevRelIndex == newRelIndex) score += 1; + if (prevStream.DisplayTitle && prevStream.DisplayTitle == stream.DisplayTitle) score += 2; + if (prevStream.Language && prevStream.Language != 'und' && prevStream.Language == stream.Language) score += 2; console.debug(`AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`); if (score > bestStreamScore && score >= 3) { @@ -2388,8 +2358,9 @@ class PlaybackManager { mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex; } } - if (streamType == 'Audio') + if (streamType == 'Audio') { mediaSource.DefaultAudioStreamIndex = bestStreamIndex; + } } else { console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`); } @@ -2693,7 +2664,7 @@ class PlaybackManager { title: item.Name }; - const backdropUrl = backdropImageUrl(apiClient, item, {}); + const backdropUrl = getItemBackdropImageUrl(apiClient, item, {}, true); if (backdropUrl) { resultInfo.backdropUrl = backdropUrl; } diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index 948912cbd8..847e2bc579 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -129,22 +129,6 @@ class PluginManager { .sort((p1, p2) => (p1.priority || 0) - (p2.priority || 0))[0]; } - #mapRoute(plugin, route) { - if (typeof plugin === 'string') { - plugin = this.pluginsList.filter((p) => { - return (p.id || p.packageName) === plugin; - })[0]; - } - - route = route.path || route; - - if (route.toLowerCase().startsWith('http')) { - return route; - } - - return '/plugins/' + plugin.id + '/' + route; - } - mapPath(plugin, path, addCacheParam) { if (typeof plugin === 'string') { plugin = this.pluginsList.filter((p) => { diff --git a/src/components/recordingcreator/recordingfields.js b/src/components/recordingcreator/recordingfields.js index d890148c3b..2fb9764f2b 100644 --- a/src/components/recordingcreator/recordingfields.js +++ b/src/components/recordingcreator/recordingfields.js @@ -191,15 +191,13 @@ function onRecordChange(e) { loading.hide(); }); } - } else { - if (hasEnabledTimer) { - loading.show(); - recordingHelper.cancelTimer(apiClient, this.TimerId, true).then(function () { - Events.trigger(self, 'recordingchanged'); - fetchData(self); - loading.hide(); - }); - } + } else if (hasEnabledTimer) { + loading.show(); + recordingHelper.cancelTimer(apiClient, this.TimerId, true).then(function () { + Events.trigger(self, 'recordingchanged'); + fetchData(self); + loading.hide(); + }); } } @@ -223,13 +221,11 @@ function onRecordSeriesChange(e) { fetchData(self); }); } - } else { - if (this.SeriesTimerId) { - apiClient.cancelLiveTvSeriesTimer(this.SeriesTimerId).then(function () { - toast(globalize.translate('RecordingCancelled')); - fetchData(self); - }); - } + } else if (this.SeriesTimerId) { + apiClient.cancelLiveTvSeriesTimer(this.SeriesTimerId).then(function () { + toast(globalize.translate('RecordingCancelled')); + fetchData(self); + }); } } diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index 0913ed25ff..c57886e0b0 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -514,10 +514,12 @@ export default function (options) { function toggleFullscreenButtons(isFullscreen) { const btnFullscreen = dialog.querySelector('.btnFullscreen'); const btnFullscreenExit = dialog.querySelector('.btnFullscreenExit'); - if (btnFullscreen) + if (btnFullscreen) { btnFullscreen.classList.toggle('hide', isFullscreen); - if (btnFullscreenExit) + } + if (btnFullscreenExit) { btnFullscreenExit.classList.toggle('hide', !isFullscreen); + } } /** diff --git a/src/components/subtitleeditor/subtitleeditor.js b/src/components/subtitleeditor/subtitleeditor.js index 572cccc1fa..ffe05ae5c3 100644 --- a/src/components/subtitleeditor/subtitleeditor.js +++ b/src/components/subtitleeditor/subtitleeditor.js @@ -194,7 +194,8 @@ function renderSearchResults(context, results) { html += ''; - const bodyClass = result.Comment || result.IsHashMatch ? 'three-line' : 'two-line'; + const hasAnyFlags = result.IsHashMatch || result.AiTranslated || result.MachineTranslated || result.Forced || result.HearingImpaired; + const bodyClass = result.Comment || hasAnyFlags ? 'three-line' : 'two-line'; html += '
'; @@ -206,16 +207,45 @@ function renderSearchResults(context, results) { } if (result.DownloadCount != null) { - html += '' + globalize.translate('DownloadsValue', result.DownloadCount) + ''; + html += '' + globalize.translate('DownloadsValue', result.DownloadCount) + ''; } + + if (result.FrameRate) { + html += '' + globalize.translate('Framerate') + ': ' + result.FrameRate + ''; + } + html += '
'; if (result.Comment) { - html += '
' + escapeHtml(result.Comment) + '
'; + html += '
' + escapeHtml(result.Comment) + '
'; } - if (result.IsHashMatch) { - html += '
' + globalize.translate('PerfectMatch') + '
'; + if (hasAnyFlags) { + html += '
'; + + const spanOpen = ''; + + if (result.IsHashMatch) { + html += spanOpen + globalize.translate('PerfectMatch') + ''; + } + + if (result.AiTranslated) { + html += spanOpen + globalize.translate('AiTranslated') + ''; + } + + if (result.MachineTranslated) { + html += spanOpen + globalize.translate('MachineTranslated') + ''; + } + + if (result.Forced) { + html += spanOpen + globalize.translate('ForeignPartsOnly') + ''; + } + + if (result.HearingImpaired) { + html += spanOpen + globalize.translate('HearingImpairedShort') + ''; + } + + html += '
'; } html += ''; diff --git a/src/components/subtitleeditor/subtitleeditor.scss b/src/components/subtitleeditor/subtitleeditor.scss index 08e6faffba..578e82ddd2 100644 --- a/src/components/subtitleeditor/subtitleeditor.scss +++ b/src/components/subtitleeditor/subtitleeditor.scss @@ -1,3 +1,11 @@ .originalSubtitleFileLabel { margin-right: 1em; } + +.subtitleFeaturePillow { + background: #00a4dc; + color: #000; + padding: 0.3em 1em; + border-radius: 1000em; + margin-right: 0.25em; +} diff --git a/src/components/viewContainer.js b/src/components/viewContainer.js index a5547517a7..c2156a7d58 100644 --- a/src/components/viewContainer.js +++ b/src/components/viewContainer.js @@ -73,12 +73,10 @@ export function loadView(options) { } else { mainAnimatedPages.replaceChild(view, currentPage); } + } else if (newViewInfo.hasScript && window.$) { + view = $(view).appendTo(mainAnimatedPages)[0]; } else { - if (newViewInfo.hasScript && window.$) { - view = $(view).appendTo(mainAnimatedPages)[0]; - } else { - mainAnimatedPages.appendChild(view); - } + mainAnimatedPages.appendChild(view); } if (options.type) { diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 372000a685..bae3880f28 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -484,13 +484,11 @@ window.DashboardPage = { if (nowPlayingItem.Artists?.length) { bottomText = topText; topText = escapeHtml(nowPlayingItem.Artists[0]); - } else { - if (nowPlayingItem.SeriesName || nowPlayingItem.Album) { - bottomText = topText; - topText = escapeHtml(nowPlayingItem.SeriesName || nowPlayingItem.Album); - } else if (nowPlayingItem.ProductionYear) { - bottomText = nowPlayingItem.ProductionYear; - } + } else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) { + bottomText = topText; + topText = escapeHtml(nowPlayingItem.SeriesName || nowPlayingItem.Album); + } else if (nowPlayingItem.ProductionYear) { + bottomText = nowPlayingItem.ProductionYear; } if (nowPlayingItem.ImageTags?.Logo) { diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index f28064e09a..1c5ede9531 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -110,10 +110,11 @@ function load(page, devices) { deviceHtml += '
'; if (canDelete(device.Id)) { - if (globalize.getIsRTL()) + if (globalize.getIsRTL()) { deviceHtml += '
'; - else + } else { deviceHtml += '
'; + } deviceHtml += ''; deviceHtml += '
'; } diff --git a/src/controllers/dashboard/dlna/profile.js b/src/controllers/dashboard/dlna/profile.js index 47583f2510..0a88f4214e 100644 --- a/src/controllers/dashboard/dlna/profile.js +++ b/src/controllers/dashboard/dlna/profile.js @@ -272,10 +272,8 @@ function renderDirectPlayProfiles(page, profiles) { if (profile.Type == 'Video') { html += '

' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '

'; html += '

' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '

'; - } else { - if (profile.Type == 'Audio') { - html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; - } + } else if (profile.Type == 'Audio') { + html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; } html += ''; @@ -333,10 +331,8 @@ function renderTranscodingProfiles(page, profiles) { if (profile.Type == 'Video') { html += '

' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '

'; html += '

' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '

'; - } else { - if (profile.Type == 'Audio') { - html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; - } + } else if (profile.Type == 'Audio') { + html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; } html += ''; @@ -561,10 +557,8 @@ function renderResponseProfiles(page, profiles) { if (profile.Type == 'Video') { html += '

' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '

'; html += '

' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '

'; - } else { - if (profile.Type == 'Audio') { - html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; - } + } else if (profile.Type == 'Audio') { + html += '

' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '

'; } if (profile.Conditions?.length) { diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 08203b76f6..51ca3af712 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -309,9 +309,7 @@ function getVirtualFolderHtml(page, virtualFolder, index) { html += '
'; // always show menu unless explicitly hidden if (virtualFolder.showMenu !== false) { - let dirTextAlign = 'right'; - if (globalize.getIsRTL()) - dirTextAlign = 'left'; + const dirTextAlign = globalize.getIsRTL() ? 'left' : 'right'; html += '
'; html += ''; html += '
'; diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index 13d0132c94..f6442710fc 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -83,10 +83,11 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) { html += '
'; if (configPage || plugin.CanUninstall) { - if (globalize.getIsRTL()) + if (globalize.getIsRTL()) { html += '
'; - else + } else { html += '
'; + } html += ''; html += '
'; } diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index 8ae447aabc..f17bc5c935 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -57,9 +57,7 @@ function populateList(page, tasks) { html += ''; html += ''; html += '
'; - let textAlignStyle = 'left'; - if (globalize.getIsRTL()) - textAlignStyle = 'right'; + const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left'; html += ""; html += "

" + task.Name + '

'; html += "
" + getTaskProgressHtml(task) + '
'; diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 37f5d3108a..102940f65c 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -36,6 +36,7 @@ import Dashboard from '../../utils/dashboard'; import ServerConnections from '../../components/ServerConnections'; import confirm from '../../components/confirm/confirm'; import { download } from '../../scripts/fileDownloader'; +import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; function autoFocus(container) { import('../../components/autoFocuser').then(({ default: autoFocuser }) => { @@ -501,34 +502,12 @@ function renderDetailPageBackdrop(page, item, apiClient) { return false; } - let imgUrl; let hasbackdrop = false; const itemBackdropElement = page.querySelector('#itemBackdrop'); - if (item.BackdropImageTags?.length) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.BackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { - imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.ParentBackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ImageTags?.Primary) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Primary', - maxWidth: dom.getScreenWidth(), - tag: item.ImageTags.Primary - }); + const imgUrl = getItemBackdropImageUrl(apiClient, item, { maxWitdh: dom.getScreenWidth() }, false); + + if (imgUrl) { imageLoader.lazyImage(itemBackdropElement, imgUrl); hasbackdrop = true; } else { diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 0feb6e3752..4fd441975b 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -306,8 +306,7 @@ export default function (view) { function onHideAnimationComplete(e) { const elem = e.target; - if (elem != osdBottomElement) - return; + if (elem != osdBottomElement) return; elem.classList.add('hide'); dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, { once: true @@ -392,11 +391,9 @@ export default function (view) { case 'left': if (currentVisibleMenu === 'osd') { showOsd(); - } else { - if (!currentVisibleMenu) { - e.preventDefault(); - playbackManager.rewind(player); - } + } else if (!currentVisibleMenu) { + e.preventDefault(); + playbackManager.rewind(player); } break; diff --git a/src/elements/emby-itemscontainer/emby-itemscontainer.js b/src/elements/emby-itemscontainer/emby-itemscontainer.js index b951fa7d2f..dd40a80531 100644 --- a/src/elements/emby-itemscontainer/emby-itemscontainer.js +++ b/src/elements/emby-itemscontainer/emby-itemscontainer.js @@ -285,10 +285,8 @@ ItemsContainerPrototype.attachedCallback = function () { if (browser.touch) { this.addEventListener('contextmenu', disableEvent); - } else { - if (this.getAttribute('data-contextmenu') !== 'false') { - this.addEventListener('contextmenu', onContextMenu); - } + } else if (this.getAttribute('data-contextmenu') !== 'false') { + this.addEventListener('contextmenu', onContextMenu); } if (layoutManager.desktop || layoutManager.mobile && this.getAttribute('data-multiselect') !== 'false') { diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js index ec93e61fd6..afd9490920 100644 --- a/src/elements/emby-slider/emby-slider.js +++ b/src/elements/emby-slider/emby-slider.js @@ -30,8 +30,9 @@ function mapClientToFraction(range, clientX) { const rect = range.sliderBubbleTrack.getBoundingClientRect(); let fraction = (clientX - rect.left) / rect.width; - if (globalize.getIsElementRTL(range)) + if (globalize.getIsElementRTL(range)) { fraction = (rect.right - clientX) / rect.width; + } // Snap to step const valueRange = range.max - range.min; @@ -490,10 +491,11 @@ EmbySliderPrototype.setKeyboardSteps = function (stepDown, stepUp) { function setRange(elem, startPercent, endPercent) { const style = elem.style; - if (globalize.getIsRTL()) + if (globalize.getIsRTL()) { style.right = Math.max(startPercent, 0) + '%'; - else + } else { style.left = Math.max(startPercent, 0) + '%'; + } const widthPercent = endPercent - startPercent; style.width = Math.max(Math.min(widthPercent, 100), 0) + '%'; diff --git a/src/index.jsx b/src/index.jsx index 9bcbffa05c..49d95c56c6 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -227,10 +227,8 @@ async function onAppReady() { document.body.appendChild(localStyle); } localStyle.textContent = localCss; - } else { - if (localStyle) { - localStyle.textContent = ''; - } + } else if (localStyle) { + localStyle.textContent = ''; } }; diff --git a/src/libraries/navdrawer/navdrawer.js b/src/libraries/navdrawer/navdrawer.js index ef6dd1e04f..9fcaf9c9d8 100644 --- a/src/libraries/navdrawer/navdrawer.js +++ b/src/libraries/navdrawer/navdrawer.js @@ -130,17 +130,15 @@ class NavDrawer { if (this.isPeeking) { this.onMenuTouchMove(e); - } else { - if ((getTouches(e)[0]?.clientX || 0) <= options.handleSize) { - this.isPeeking = true; + } else if ((getTouches(e)[0]?.clientX || 0) <= options.handleSize) { + this.isPeeking = true; - if (e.type === 'touchstart') { - dom.removeEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {}); - dom.addEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {}); - } - - this.onMenuTouchStart(e); + if (e.type === 'touchstart') { + dom.removeEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {}); + dom.addEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {}); } + + this.onMenuTouchStart(e); } }; @@ -206,10 +204,11 @@ class NavDrawer { options.target.classList.add('touch-menu-la'); options.target.style.width = options.width + 'px'; - if (globalize.getIsRTL()) + if (globalize.getIsRTL()) { options.target.style.right = -options.width + 'px'; - else + } else { options.target.style.left = -options.width + 'px'; + } if (!options.disableMask) { this.mask = document.createElement('div'); @@ -246,14 +245,10 @@ class NavDrawer { } else { this.close(); } - } else { - if (this.newPos >= 100) { - this.open(); - } else { - if (this.newPos) { - this.close(); - } - } + } else if (this.newPos >= 100) { + this.open(); + } else if (this.newPos) { + this.close(); } } @@ -320,19 +315,17 @@ class NavDrawer { passive: true }); } - } else { - if (this._edgeSwipeEnabled) { - this._edgeSwipeEnabled = false; - dom.removeEventListener(this.edgeContainer, 'touchstart', this.onEdgeTouchStart, { - passive: true - }); - dom.removeEventListener(this.edgeContainer, 'touchend', this.onEdgeTouchEnd, { - passive: true - }); - dom.removeEventListener(this.edgeContainer, 'touchcancel', this.onEdgeTouchEnd, { - passive: true - }); - } + } else if (this._edgeSwipeEnabled) { + this._edgeSwipeEnabled = false; + dom.removeEventListener(this.edgeContainer, 'touchstart', this.onEdgeTouchStart, { + passive: true + }); + dom.removeEventListener(this.edgeContainer, 'touchend', this.onEdgeTouchEnd, { + passive: true + }); + dom.removeEventListener(this.edgeContainer, 'touchcancel', this.onEdgeTouchEnd, { + passive: true + }); } } } diff --git a/src/libraries/scroller.js b/src/libraries/scroller.js index 12edc468cc..147243d920 100644 --- a/src/libraries/scroller.js +++ b/src/libraries/scroller.js @@ -189,8 +189,9 @@ const scrollerFactory = function (frame, options) { // Set position limits & relatives self._pos.end = Math.max(slideeSize - frameSize, 0); - if (globalize.getIsRTL()) + if (globalize.getIsRTL()) { self._pos.end *= -1; + } } } @@ -257,12 +258,10 @@ const scrollerFactory = function (frame, options) { } else { container.scrollTo(0, Math.round(pos)); } + } else if (o.horizontal) { + container.scrollLeft = Math.round(pos); } else { - if (o.horizontal) { - container.scrollLeft = Math.round(pos); - } else { - container.scrollTop = Math.round(pos); - } + container.scrollTop = Math.round(pos); } } @@ -506,14 +505,12 @@ const scrollerFactory = function (frame, options) { // If the pointer was released, the path will not become longer and it's // definitely not a drag. If not released yet, decide on next iteration return dragging.released ? dragEnd() : undefined; - } else { + } else if (o.horizontal ? Math.abs(dragging.pathX) > Math.abs(dragging.pathY) : Math.abs(dragging.pathX) < Math.abs(dragging.pathY)) { // If dragging path is sufficiently long we can confidently start a drag // if drag is in different direction than scroll, ignore it - if (o.horizontal ? Math.abs(dragging.pathX) > Math.abs(dragging.pathY) : Math.abs(dragging.pathX) < Math.abs(dragging.pathY)) { - dragging.init = 1; - } else { - return dragEnd(); - } + dragging.init = 1; + } else { + return dragEnd(); } } diff --git a/src/plugins/bookPlayer/template.html b/src/plugins/bookPlayer/template.html index 55fd02c8a1..255b8283a2 100644 --- a/src/plugins/bookPlayer/template.html +++ b/src/plugins/bookPlayer/template.html @@ -2,12 +2,6 @@ - diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index 569e347cd2..e5663c2f08 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -27,12 +27,10 @@ function sendConnectionResult(isOk) { if (resolve) { resolve(); } + } else if (reject) { + reject(); } else { - if (reject) { - reject(); - } else { - playbackManager.removeActivePlayer(PlayerName); - } + playbackManager.removeActivePlayer(PlayerName); } } @@ -294,7 +292,7 @@ class CastPlayer { loadMedia(options, command) { if (!this.session) { console.debug('no session'); - return Promise.reject(); + return Promise.reject(new Error('no session')); } // convert items to smaller stubs to send minimal amount of information @@ -335,16 +333,27 @@ class CastPlayer { apiClient = ServerConnections.currentApiClient(); } + /* If serverAddress is localhost,this address can not be used for the cast receiver device. + * Use the local address (ULA, Unique Local Address) in that case. + */ + const serverAddress = apiClient.serverAddress(); + // eslint-disable-next-line compat/compat + const hostname = (new URL(serverAddress)).hostname; + const isLocalhost = hostname === 'localhost' || hostname.startsWith('127.') || hostname === '[::1]'; + const serverLocalAddress = isLocalhost ? apiClient.serverInfo().LocalAddress : serverAddress; + message = Object.assign(message, { userId: apiClient.getCurrentUserId(), deviceId: apiClient.deviceId(), accessToken: apiClient.accessToken(), - serverAddress: apiClient.serverAddress(), + serverAddress: serverLocalAddress, serverId: apiClient.serverId(), serverVersion: apiClient.serverVersion(), receiverName: receiverName }); + console.debug('[chromecastPlayer] message{' + message.command + '; ' + serverAddress + ' -> ' + serverLocalAddress + '}'); + const bitrateSetting = appSettings.maxChromecastBitrate(); if (bitrateSetting) { message.maxBitrate = bitrateSetting; @@ -592,7 +601,7 @@ class ChromecastPlayer { currentResolve = null; currentReject = null; - return Promise.reject(); + return Promise.reject(new Error('tryPair failed')); } } diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 58461ddc51..dd0f77da60 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -95,10 +95,11 @@ export class ComicsPlayer { onDirChanged = () => { let langDir = this.comicsPlayerSettings.langDir; - if (!langDir || langDir === 'ltr') + if (!langDir || langDir === 'ltr') { langDir = 'rtl'; - else + } else { langDir = 'ltr'; + } this.changeLanguageDirection(langDir); @@ -125,10 +126,11 @@ export class ComicsPlayer { onViewChanged = () => { let view = this.comicsPlayerSettings.pagesPerView; - if (!view || view === 1) + if (!view || view === 1) { view = 2; - else + } else { view = 1; + } this.changeView(view); diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index c6d9eeb68b..194678b7ea 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -35,6 +35,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/ba import { PluginType } from '../../types/plugin.ts'; import Events from '../../utils/events.ts'; import { includesAny } from '../../utils/container.ts'; +import { isHls } from '../../utils/mediaSource.ts'; import debounce from 'lodash-es/debounce'; /** @@ -69,12 +70,12 @@ function tryRemoveElement(elem) { } } -function enableNativeTrackSupport(currentSrc, track) { +function enableNativeTrackSupport(mediaSource, track) { if (track?.DeliveryMethod === 'Embed') { return true; } - if (browser.firefox && (currentSrc || '').toLowerCase().includes('.m3u8')) { + if (browser.firefox && isHls(mediaSource)) { return false; } @@ -168,137 +169,140 @@ const SECONDARY_TEXT_TRACK_INDEX = 1; export class HtmlVideoPlayer { /** - * @type {string} - */ + * @type {string} + */ name; /** - * @type {string} - */ + * @type {string} + */ type = PluginType.MediaPlayer; /** - * @type {string} - */ + * @type {string} + */ id = 'htmlvideoplayer'; /** - * Let any players created by plugins take priority - * - * @type {number} - */ + * Let any players created by plugins take priority + * + * @type {number} + */ priority = 1; /** - * @type {boolean} - */ + * @type {boolean} + */ isFetching = false; /** - * @type {HTMLDivElement | null | undefined} - */ + * @type {HTMLDivElement | null | undefined} + */ #videoDialog; /** - * @type {number | undefined} - */ + * @type {number | undefined} + */ #subtitleTrackIndexToSetOnPlaying; /** - * @type {number | undefined} - */ + * @type {number | undefined} + */ #secondarySubtitleTrackIndexToSetOnPlaying; /** - * @type {number | null} - */ + * @type {number | null} + */ #audioTrackIndexToSetOnPlaying; /** - * @type {null | undefined} - */ + * @type {null | undefined} + */ #currentClock; /** - * @type {any | null | undefined} - */ + * @type {any | null | undefined} + */ #currentAssRenderer; /** - * @type {null | undefined} - */ + * @type {null | undefined} + */ #customTrackIndex; /** - * @type {number | undefined} - */ + * @type {number | undefined} + */ #customSecondaryTrackIndex; /** - * @type {boolean | undefined} - */ + * @type {boolean | undefined} + */ #showTrackOffset; /** - * @type {number | undefined} - */ + * @type {number | undefined} + */ #currentTrackOffset; /** - * @type {HTMLElement | null | undefined} - */ + * @type {HTMLElement | null | undefined} + */ #secondaryTrackOffset; /** - * @type {HTMLElement | null | undefined} - */ + * @type {HTMLElement | null | undefined} + */ #videoSubtitlesElem; /** - * @type {HTMLElement | null | undefined} - */ + * @type {HTMLElement | null | undefined} + */ #videoSecondarySubtitlesElem; /** - * @type {any | null | undefined} - */ + * @type {any | null | undefined} + */ #currentTrackEvents; /** - * @type {any | null | undefined} - */ + * @type {any | null | undefined} + */ #currentSecondaryTrackEvents; /** - * @type {string[] | undefined} - */ + * @type {string[] | undefined} + */ #supportedFeatures; /** - * @type {HTMLVideoElement | null | undefined} - */ + * @type {HTMLVideoElement | null | undefined} + */ #mediaElement; /** - * @type {number} - */ + * @type {number} + */ #fetchQueue = 0; /** - * @type {string | undefined} - */ + * @type {string | undefined} + */ #currentSrc; /** - * @type {boolean | undefined} - */ + * @type {boolean | undefined} + */ #started; /** - * @type {boolean | undefined} - */ + * @type {boolean | undefined} + */ #timeUpdated; /** - * @type {number | null | undefined} - */ + * @type {number | null | undefined} + */ #currentTime; + /** - * @type {any | undefined} - */ - #flvPlayer; + * @private (used in other files) + * @type {any | undefined} + */ + _flvPlayer; + /** - * @private (used in other files) - * @type {any | undefined} - */ + * @private (used in other files) + * @type {any | undefined} + */ _hlsPlayer; /** - * @private (used in other files) - * @type {any | null | undefined} - */ + * @private (used in other files) + * @type {any | null | undefined} + */ _castPlayer; /** - * @private (used in other files) - * @type {any | undefined} - */ + * @private (used in other files) + * @type {any | undefined} + */ _currentPlayOptions; /** - * @type {any | undefined} - */ + * @type {any | undefined} + */ #lastProfile; constructor() { @@ -341,15 +345,13 @@ export class HtmlVideoPlayer { * @private */ updateVideoUrl(streamInfo) { - const isHls = streamInfo.url.toLowerCase().includes('.m3u8'); - const mediaSource = streamInfo.mediaSource; const item = streamInfo.item; // Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts // This will start the transcoding process before actually feeding the video url into the player // Edit: Also seeing stalls from hls.js - if (mediaSource && item && !mediaSource.RunTimeTicks && isHls && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) { + if (mediaSource && item && !mediaSource.RunTimeTicks && isHls(mediaSource) && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) { const hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8'); loading.show(); @@ -408,7 +410,7 @@ export class HtmlVideoPlayer { flvPlayer.attachMediaElement(elem); flvPlayer.load(); - this.#flvPlayer = flvPlayer; + this._flvPlayer = flvPlayer; // This is needed in setCurrentTrackElement this.#currentSrc = url; @@ -513,7 +515,7 @@ export class HtmlVideoPlayer { elem.crossOrigin = crossOrigin; } - if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && val.includes('.m3u8')) { + if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && isHls(options.mediaSource)) { return this.setSrcWithHlsJs(elem, options, val); } else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') { return this.setSrcWithFlvJs(elem, options, val); @@ -864,11 +866,9 @@ export class HtmlVideoPlayer { if (Screenfull.isEnabled) { Screenfull.exit(); - } else { + } else if (document.webkitIsFullScreen && document.webkitCancelFullscreen) { // iOS Safari - if (document.webkitIsFullScreen && document.webkitCancelFullscreen) { - document.webkitCancelFullscreen(); - } + document.webkitCancelFullscreen(); } } @@ -1106,15 +1106,14 @@ export class HtmlVideoPlayer { tryRemoveElement(this.#videoSecondarySubtitlesElem); this.#videoSecondarySubtitlesElem = null; } - } else { // destroy all - if (this.#videoSubtitlesElem) { - const subtitlesContainer = this.#videoSubtitlesElem.parentNode; - if (subtitlesContainer) { - tryRemoveElement(subtitlesContainer); - } - this.#videoSubtitlesElem = null; - this.#videoSecondarySubtitlesElem = null; + } else if (this.#videoSubtitlesElem) { + // destroy all + const subtitlesContainer = this.#videoSubtitlesElem.parentNode; + if (subtitlesContainer) { + tryRemoveElement(subtitlesContainer); } + this.#videoSubtitlesElem = null; + this.#videoSecondarySubtitlesElem = null; } } @@ -1561,7 +1560,7 @@ export class HtmlVideoPlayer { })[0]; this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex); - if (enableNativeTrackSupport(this.#currentSrc, track)) { + if (enableNativeTrackSupport(this._currentPlayOptions?.mediaSource, track)) { if (streamIndex !== -1) { this.setCueAppearance(); } @@ -1665,7 +1664,13 @@ export class HtmlVideoPlayer { } } - return Promise.resolve(dlg.querySelector('video')); + const videoElement = dlg.querySelector('video'); + if (options.backdropUrl) { + // update backdrop image + videoElement.poster = options.backdropUrl; + } + + return Promise.resolve(videoElement); } } @@ -1812,10 +1817,8 @@ export class HtmlVideoPlayer { } else { Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default); } - } else { - if (video?.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') { - video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline'); - } + } else if (video?.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') { + video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline'); } } diff --git a/src/plugins/youtubePlayer/plugin.js b/src/plugins/youtubePlayer/plugin.js index 47492468aa..7c9b68cd95 100644 --- a/src/plugins/youtubePlayer/plugin.js +++ b/src/plugins/youtubePlayer/plugin.js @@ -352,10 +352,8 @@ class YoutubePlayer { if (currentYoutubePlayer) { currentYoutubePlayer.mute(); } - } else { - if (currentYoutubePlayer) { - currentYoutubePlayer.unMute(); - } + } else if (currentYoutubePlayer) { + currentYoutubePlayer.unMute(); } } isMuted() { diff --git a/src/scripts/autocast.js b/src/scripts/autocast.js index 9ed7de5df7..2633587ba5 100644 --- a/src/scripts/autocast.js +++ b/src/scripts/autocast.js @@ -26,7 +26,7 @@ export function isEnabled() { const playerId = localStorage.getItem('autocastPlayerId'); const currentPlayerInfo = playbackManager.getPlayerInfo(); - return (currentPlayerInfo && playerId && currentPlayerInfo.id === playerId); + return playerId && currentPlayerInfo?.id === playerId; } function onOpen() { diff --git a/src/scripts/browser.js b/src/scripts/browser.js index 47b0635d52..15898f03b1 100644 --- a/src/scripts/browser.js +++ b/src/scripts/browser.js @@ -148,14 +148,11 @@ let _supportsCssAnimation; let _supportsCssAnimationWithPrefix; function supportsCssAnimation(allowPrefix) { // TODO: Assess if this is still needed, as all of our targets should natively support CSS animations. - if (allowPrefix) { - if (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false) { - return _supportsCssAnimationWithPrefix; - } - } else { - if (_supportsCssAnimation === true || _supportsCssAnimation === false) { - return _supportsCssAnimation; - } + if (allowPrefix && (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false)) { + return _supportsCssAnimationWithPrefix; + } + if (_supportsCssAnimation === true || _supportsCssAnimation === false) { + return _supportsCssAnimation; } let animation = false; @@ -187,13 +184,13 @@ function supportsCssAnimation(allowPrefix) { const uaMatch = function (ua) { ua = ua.toLowerCase(); - const match = /(edg)[ /]([\w.]+)/.exec(ua) + const match = /(chrome)[ /]([\w.]+)/.exec(ua) + || /(edg)[ /]([\w.]+)/.exec(ua) || /(edga)[ /]([\w.]+)/.exec(ua) || /(edgios)[ /]([\w.]+)/.exec(ua) || /(edge)[ /]([\w.]+)/.exec(ua) || /(opera)[ /]([\w.]+)/.exec(ua) || /(opr)[ /]([\w.]+)/.exec(ua) - || /(chrome)[ /]([\w.]+)/.exec(ua) || /(safari)[ /]([\w.]+)/.exec(ua) || /(firefox)[ /]([\w.]+)/.exec(ua) || ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) diff --git a/src/scripts/browserDeviceProfile.js b/src/scripts/browserDeviceProfile.js index ff8bcfcca2..c00e188d4b 100644 --- a/src/scripts/browserDeviceProfile.js +++ b/src/scripts/browserDeviceProfile.js @@ -718,6 +718,15 @@ export default function (options) { enableFmp4Hls = false; } if (hlsInFmp4VideoCodecs.length && hlsInFmp4VideoAudioCodecs.length && enableFmp4Hls) { + // HACK: Since there is no filter for TS/MP4 in the API, specify HLS support in general and rely on retry after DirectPlay error + // FIXME: Need support for {Container: 'mp4', Protocol: 'hls'} or {Container: 'hls', SubContainer: 'mp4'} + profile.DirectPlayProfiles.push({ + Container: 'hls', + Type: 'Video', + VideoCodec: hlsInFmp4VideoCodecs.join(','), + AudioCodec: hlsInFmp4VideoAudioCodecs.join(',') + }); + profile.TranscodingProfiles.push({ Container: 'mp4', Type: 'Video', @@ -732,6 +741,15 @@ export default function (options) { } if (hlsInTsVideoCodecs.length && hlsInTsVideoAudioCodecs.length) { + // HACK: Since there is no filter for TS/MP4 in the API, specify HLS support in general and rely on retry after DirectPlay error + // FIXME: Need support for {Container: 'ts', Protocol: 'hls'} or {Container: 'hls', SubContainer: 'ts'} + profile.DirectPlayProfiles.push({ + Container: 'hls', + Type: 'Video', + VideoCodec: hlsInTsVideoCodecs.join(','), + AudioCodec: hlsInTsVideoAudioCodecs.join(',') + }); + profile.TranscodingProfiles.push({ Container: 'ts', Type: 'Video', diff --git a/src/scripts/datetime.js b/src/scripts/datetime.js index 3903b58638..f1243a5076 100644 --- a/src/scripts/datetime.js +++ b/src/scripts/datetime.js @@ -230,7 +230,6 @@ export function getDisplayTime(date) { const timeLower = time.toLowerCase(); if (timeLower.indexOf('am') !== -1 || timeLower.indexOf('pm') !== -1) { - time = timeLower; let hour = date.getHours() % 12; const suffix = date.getHours() > 11 ? 'pm' : 'am'; if (!hour) { diff --git a/src/scripts/globalize.js b/src/scripts/globalize.js index 21260e2be4..1743890e1b 100644 --- a/src/scripts/globalize.js +++ b/src/scripts/globalize.js @@ -64,8 +64,9 @@ function checkAndProcessDir(culture) { function setDocumentDirection(direction) { document.getElementsByTagName('body')[0].setAttribute('dir', direction); document.getElementsByTagName('html')[0].setAttribute('dir', direction); - if (direction === Direction.rtl) + if (direction === Direction.rtl) { import('../styles/rtl.scss'); + } } export function getIsElementRTL(element) { diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index b77dfa92e1..0f3b4a73e1 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -835,26 +835,24 @@ function updateMenuForPageType(isDashboardPage, isLibraryPage) { bodyClassList.remove('dashboardDocument'); bodyClassList.remove('hideMainDrawer'); + if (navDrawerInstance) { + navDrawerInstance.setEdgeSwipeEnabled(true); + } + } else if (isDashboardPage) { + bodyClassList.remove('libraryDocument'); + bodyClassList.add('dashboardDocument'); + bodyClassList.remove('hideMainDrawer'); + if (navDrawerInstance) { navDrawerInstance.setEdgeSwipeEnabled(true); } } else { - if (isDashboardPage) { - bodyClassList.remove('libraryDocument'); - bodyClassList.add('dashboardDocument'); - bodyClassList.remove('hideMainDrawer'); + bodyClassList.remove('libraryDocument'); + bodyClassList.remove('dashboardDocument'); + bodyClassList.add('hideMainDrawer'); - if (navDrawerInstance) { - navDrawerInstance.setEdgeSwipeEnabled(true); - } - } else { - bodyClassList.remove('libraryDocument'); - bodyClassList.remove('dashboardDocument'); - bodyClassList.add('hideMainDrawer'); - - if (navDrawerInstance) { - navDrawerInstance.setEdgeSwipeEnabled(false); - } + if (navDrawerInstance) { + navDrawerInstance.setEdgeSwipeEnabled(false); } } } diff --git a/src/scripts/scrollHelper.js b/src/scripts/scrollHelper.js index 725df86434..9f6a870ed3 100644 --- a/src/scripts/scrollHelper.js +++ b/src/scripts/scrollHelper.js @@ -54,12 +54,10 @@ export function toCenter(container, elem, horizontal, skipWhenVisible) { } else { container.scrollTo(0, pos.center); } + } else if (horizontal) { + container.scrollLeft = Math.round(pos.center); } else { - if (horizontal) { - container.scrollLeft = Math.round(pos.center); - } else { - container.scrollTop = Math.round(pos.center); - } + container.scrollTop = Math.round(pos.center); } } @@ -76,12 +74,10 @@ export function toStart(container, elem, horizontal, skipWhenVisible) { } else { container.scrollTo(0, pos.start); } + } else if (horizontal) { + container.scrollLeft = Math.round(pos.start); } else { - if (horizontal) { - container.scrollLeft = Math.round(pos.start); - } else { - container.scrollTop = Math.round(pos.start); - } + container.scrollTop = Math.round(pos.start); } } diff --git a/src/strings/bg-bg.json b/src/strings/bg-bg.json index 4af828da24..f9f61bb285 100644 --- a/src/strings/bg-bg.json +++ b/src/strings/bg-bg.json @@ -1474,5 +1474,9 @@ "AllowSegmentDeletionHelp": "Изтриване на сегментите след изпращане към клиента. Това предотвратява нуждата за съхраняване на целия транскодиран файл на диска. Работи само с включено подтискане. Изключете тази функция ако изпитате проблеми с възпоизвеждането.", "LabelThrottleDelaySecondsHelp": "Време в секунди след което транскодера ще бъде подтиснат. Трябва да е достатъчно голям за клиента да поддържа здрав буфер. Работи само ако подтискането е включено.", "LabelSegmentKeepSeconds": "Време в което да се запазят сегменти", - "LabelSegmentKeepSecondsHelp": "Време в секунди за което сегментите трябва да се пазят след презаписване. Трябва да е повече от \"Подтискане след\". Работи само ако 'Изтриване на сегменти' е включено." + "LabelSegmentKeepSecondsHelp": "Време в секунди за което сегментите трябва да се пазят след презаписване. Трябва да е повече от \"Подтискане след\". Работи само ако 'Изтриване на сегменти' е включено.", + "EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).", + "EnableAudioNormalization": "Нормализация на звука", + "Unknown": "Неизвестен", + "LabelThrottleDelaySeconds": "Ограничи след" } diff --git a/src/strings/cs.json b/src/strings/cs.json index 359bf7d812..f80641a27f 100644 --- a/src/strings/cs.json +++ b/src/strings/cs.json @@ -1763,5 +1763,12 @@ "LabelSegmentKeepSeconds": "Doba ponechání částí", "LabelSegmentKeepSecondsHelp": "Čas v sekundách, po který budou části překódovaného souboru uloženy. Musí být delší než čas určený v \"Omezit po\". Funguje pouze při zapnuté funkci Odstranění částí.", "LabelBackdropScreensaverInterval": "Interval šetřiče \"Pozadí\"", - "LabelBackdropScreensaverIntervalHelp": "Čas v sekundách mezi změnou pozadí při použití šetřiče \"Pozadí\"." + "LabelBackdropScreensaverIntervalHelp": "Čas v sekundách mezi změnou pozadí při použití šetřiče \"Pozadí\".", + "AllowAv1Encoding": "Povolit kódování do formátu AV1", + "GridView": "Mřížka", + "ListView": "Seznam", + "MachineTranslated": "Strojově přeloženo", + "ForeignPartsOnly": "Pouze vynucené", + "HearingImpairedShort": "Titulky pro neslyšící", + "AiTranslated": "Přeložené pomocí UI" } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 424c283f0c..14675b077e 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -298,6 +298,7 @@ "Genres": "Genres", "GetThePlugin": "Get the Plugin", "GoogleCastUnsupported": "Google Cast Unsupported", + "GridView": "Grid View", "GroupBySeries": "Group by series", "GroupVersions": "Group versions", "GuestStar": "Guest star", @@ -999,6 +1000,7 @@ "LeaveBlankToNotSetAPassword": "You can leave this field blank to set no password.", "LibraryAccessHelp": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.", "List": "List", + "ListView": "List View", "ListPaging": "{0}-{1} of {2}", "Live": "Live", "LiveBroadcasts": "Live broadcasts", @@ -1717,5 +1719,9 @@ "TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode.", "Unknown": "Unknown", "AllowAv1Encoding": "Allow encoding in AV1 format", + "AiTranslated": "AI Translated", + "MachineTranslated": "Machine Translated", + "ForeignPartsOnly": "Forced/Foreign parts only", + "HearingImpairedShort": "HI/SDH", "LabelIsHearingImpaired": "For hearing impaired (SDH)" } diff --git a/src/strings/fi.json b/src/strings/fi.json index 3d2b0a7e18..4c69d67800 100644 --- a/src/strings/fi.json +++ b/src/strings/fi.json @@ -1232,7 +1232,7 @@ "EnableTonemapping": "Käytä sävykartoitusta", "EnableBlurHashHelp": "Kuvat, joita ladataan vielä, näytetään yksilöllisellä paikkamerkillä.", "EnableBlurHash": "Ota sumennetut paikkamerkit käyttöön kuville", - "AllowTonemappingHelp": "Sävykartoitus voi muuttaa videon dynaamisen alueen HDR:stä SDR:ksi säilyttäen samalla kuvan yksityiskohdat ja värit, jotka ovat kohtauksen alkuperäisen ilmeen kannalta erittäin tärkeitä. Toimii tällä hetkellä vain 10bit HDR10, HLG ja Dovi -videoiden kanssa ja edellyttää soveltuvaa OpenCL- tai CUDA-suoritusalustaa.", + "AllowTonemappingHelp": "Sävykartoitus voi muuttaa videon dynaamisen alueen HDR:stä SDR:ksi säilyttäen samalla kuvan yksityiskohdat ja värit, jotka ovat kohtauksen alkuperäisen ilmeen kannalta erittäin tärkeitä. Toimii tällä hetkellä vain 10-bit HDR10-, HLG- ja DoVi-videoiden kanssa ja edellyttää soveltuvaa OpenCL- tai CUDA-suoritusalustaa.", "LabelffmpegPathHelp": "FFmpeg-sovellustiedoston tai -kansion tiedostosijainti.", "LabelKodiMetadataEnablePathSubstitutionHelp": "Mahdollistaa kuvien tiedostosijaintien korvauksen palvelimen korvausasetuksien perusteella.", "ThumbCard": "Pienoiskortti", @@ -1761,5 +1761,6 @@ "LabelSegmentKeepSeconds": "Osioiden säilytysaika", "LabelSegmentKeepSecondsHelp": "Aika sekunteina, jonka osiot säilytetään ennen päällekirjoitusta. Oltava \"Rahoita kun on kulunut\" -aikaa suurempi. Toimii vain osioiden poiston ollessa käytössä.", "LabelBackdropScreensaverInterval": "Taustanäytönsäästäjän ajoitus", - "LabelBackdropScreensaverIntervalHelp": "Aika sekuntteina, jonka kuluttua kuva vaihtuu taustanäytönsäästäjää käytettäessä." + "LabelBackdropScreensaverIntervalHelp": "Aika sekuntteina, jonka kuluttua kuva vaihtuu taustanäytönsäästäjää käytettäessä.", + "AllowAv1Encoding": "Salli enkoodaus AV1-muodossa" } diff --git a/src/strings/fr.json b/src/strings/fr.json index 1f206c563b..ea1d558809 100644 --- a/src/strings/fr.json +++ b/src/strings/fr.json @@ -1384,7 +1384,7 @@ "LabelTonemappingRange": "Gamme de mappage tonal", "TonemappingAlgorithmHelp": "Le mappage tonal peut être affiné. Si vous n'êtes pas familier avec ces réglages, il est conseillé de choisir les valeurs recommandées. L'algorithme de mappage recommandé est 'BT.2390'.", "LabelTonemappingAlgorithm": "Sélectionner l'algorithme de mappage tonal à utiliser", - "AllowTonemappingHelp": "Le mappage tonal est capable de convertir une gamme dynamique HDR en SDR tout en maintenant les détails et les couleurs d'image si importants au rendu de la scène originale. Pour le moment, ne fonctionne qu'avec les vidéos 10bits HDR10, HLG, DoVi et requiert les environnements d'exécution OpenCL et CUDA correspondant.", + "AllowTonemappingHelp": "Le mappage tonal est capable de convertir une gamme dynamique HDR en SDR tout en maintenant les détails et les couleurs d'image si importants au rendu de la scène originale. Pour le moment, ne fonctionne qu'avec les vidéos HDR10 10bits, HLG, DoVi et requiert les environnements d'exécution OpenCL et CUDA correspondant.", "EnableTonemapping": "Activer le mappage tonal", "LabelOpenclDeviceHelp": "Ce dispositif OpenCL est utilisé pour le mappage tonal. La partie à gauche du point est le numéro de plate-forme et la partie à droite est le numéro du dispositif sur la plate-forme. La valeur par défaut est 0,0. Le fichier de l'application FFmpeg contenant l'accélération matérielle OpenCL est nécessaire.", "LabelOpenclDevice": "Dispositif OpenCL", @@ -1748,7 +1748,7 @@ "LabelLevel": "Niveau", "LabelMediaDetails": "Détails du média", "LabelSystem": "Système", - "LogLevel.Trace": "Trace", + "LogLevel.Trace": "Suivi", "LogLevel.Debug": "Debug", "LogLevel.Information": "Information", "LogLevel.Warning": "Avertissement", @@ -1763,5 +1763,6 @@ "LabelSegmentKeepSeconds": "Durée de conservation des segments", "LabelSegmentKeepSecondsHelp": "Durée en secondes de conservation des segments avant écrasement. La valeur doit être supérieure au délai d'ajustement. Ne fonctionne que si la suppression des segments est activée.", "LabelBackdropScreensaverIntervalHelp": "Le temps en secondes entre différents fonds d'écran lors de l'utilisation de l'économiseur d'écran à fonds d'écran.", - "LabelBackdropScreensaverInterval": "Intervalle de l'économiseur d'écran à fonds d'écran" + "LabelBackdropScreensaverInterval": "Intervalle de l'économiseur d'écran à fonds d'écran", + "AllowAv1Encoding": "Autoriser l'encodage au format AV1" } diff --git a/src/strings/nl.json b/src/strings/nl.json index a69adfcf0b..ba9031fc88 100644 --- a/src/strings/nl.json +++ b/src/strings/nl.json @@ -169,7 +169,7 @@ "EnableThemeSongsHelp": "Speel titelmuziek af tijdens het bladeren door de bibliotheek.", "EnableThemeVideosHelp": "Speel titelfilms af op de achtergrond tijdens het bladeren door de bibliotheek.", "Ended": "Gestopt", - "EndsAtValue": "Eindigt om {0}", + "EndsAtValue": "Afgelopen om {0}", "Episodes": "Afleveringen", "ErrorAddingListingsToSchedulesDirect": "Er ging iets mis bij het toevoegen van de lineup aan uw Schedules Direct account. Schedules Direct staat maar een beperkt aantal lineups per account toe. Het kan nodig zijn dat u zich aan moet melden op de Schedules Direct-website en andere lineups moet verwijderen voordat u verder kunt.", "ErrorAddingMediaPathToVirtualFolder": "Er ging iets mis bij het toevoegen van het mediapad. Controleer of het pad klopt en of Jellyfin toegang heeft tot de locatie.", @@ -1762,5 +1762,6 @@ "LabelThrottleDelaySecondsHelp": "Tijd in seconden waarna de transcoder wordt afgeknepen. Deze tijd moet voldoende lang zijn zodat de cliënt een gezonde buffer in stand kan houden. Werkt alleen als afknijpen is ingeschakeld.", "LabelSegmentKeepSeconds": "Bewaartijd segmenten", "LabelBackdropScreensaverIntervalHelp": "Het aantal seconden dat een achtergrondafbeelding wordt getoond als onderdeel van de schermbeveiliging.", - "LabelBackdropScreensaverInterval": "Interval schermbeveiliging" + "LabelBackdropScreensaverInterval": "Interval schermbeveiliging", + "AllowAv1Encoding": "Coderen in AV1-formaat toestaan" } diff --git a/src/strings/uk.json b/src/strings/uk.json index d74f61e673..c0bc6c9087 100644 --- a/src/strings/uk.json +++ b/src/strings/uk.json @@ -1760,5 +1760,6 @@ "LabelSegmentKeepSecondsHelp": "Час у секундах, протягом якого сегменти мають зберігатися перед перезаписом. Має бути більшим за \"Обмежити після\". Працює тільки якщо увімкнено видалення сегментів.", "LabelSegmentKeepSeconds": "Час збереження сегментів", "LabelBackdropScreensaverIntervalHelp": "Час у секундах між різними фонами при використанні фонової заставки.", - "LabelBackdropScreensaverInterval": "Інтервал між фоновими заставками" + "LabelBackdropScreensaverInterval": "Інтервал між фоновими заставками", + "AllowAv1Encoding": "Дозволити кодування у форматі AV1" } diff --git a/src/types/library.ts b/src/types/library.ts index 313a5ebe3e..2994dd3ca5 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -3,6 +3,7 @@ import type { VideoType } from '@jellyfin/sdk/lib/generated-client/models/video- import type { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import type { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client'; export type ParentId = string | null | undefined; @@ -23,12 +24,18 @@ interface Filters { Years?: number[]; } +export enum ViewMode { + GridView = 'grid', + ListView = 'list', +} + export interface LibraryViewSettings { SortBy: ItemSortBy; SortOrder: SortOrder; StartIndex: number; CardLayout: boolean; - ImageType: string; + ImageType: ImageType; + ViewMode: ViewMode; ShowTitle: boolean; ShowYear?: boolean; Filters?: Filters; diff --git a/src/utils/jellyfin-apiclient/backdropImage.ts b/src/utils/jellyfin-apiclient/backdropImage.ts new file mode 100644 index 0000000000..2f9b3c5da8 --- /dev/null +++ b/src/utils/jellyfin-apiclient/backdropImage.ts @@ -0,0 +1,52 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { randomInt } from '../number'; + +export interface ScaleImageOptions { + maxWidth?: number; + width?: number; + maxHeight?: number; + height?: number; + fillWidth?: number; + fillHeight?: number; + quality?: number; +} + +/** + * Returns the url of the first or a random backdrop image of an item. + * If the item has no backdrop image, the url of the first or a random backdrop image of the parent item is returned. + * Falls back to the primary image (cover) of the item, if neither the item nor it's parent have at least one backdrop image. + * Returns undefined if no usable image was found. + * @param apiClient The ApiClient to generate the url. + * @param item The item for which the backdrop image is requested. + * @param options Optional; allows to scale the backdrop image. + * @param random If set to true and the item has more than one backdrop, a random image is returned. + * @returns The url of the first or a random backdrop image of the provided item. + */ +export const getItemBackdropImageUrl = (apiClient: ApiClient, item: BaseItemDto, options: ScaleImageOptions = {}, random = false): string | undefined => { + if (item.Id && item.BackdropImageTags?.length) { + const backdropImgIndex = random ? randomInt(0, item.BackdropImageTags.length - 1) : 0; + return apiClient.getScaledImageUrl(item.Id, { + type: ImageType.Backdrop, + index: backdropImgIndex, + tag: item.BackdropImageTags[backdropImgIndex], + ...options + }); + } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags?.length) { + const backdropImgIndex = random ? randomInt(0, item.ParentBackdropImageTags.length - 1) : 0; + return apiClient.getScaledImageUrl(item.ParentBackdropItemId, { + type: ImageType.Backdrop, + index: backdropImgIndex, + tag: item.ParentBackdropImageTags[backdropImgIndex], + ...options + }); + } else if (item.Id && item.ImageTags?.Primary) { + return apiClient.getScaledImageUrl(item.Id, { + type: ImageType.Primary, + tag: item.ImageTags.Primary, + ...options + }); + } + return undefined; +}; diff --git a/src/utils/mediaSource.ts b/src/utils/mediaSource.ts new file mode 100644 index 0000000000..8238ef1d4d --- /dev/null +++ b/src/utils/mediaSource.ts @@ -0,0 +1,10 @@ +import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client'; + +/** + * Checks if the media source is an HLS stream. + * @param mediaSource The media source. + * @returns _true_ if the media source is an HLS stream, _false_ otherwise. + */ +export function isHls(mediaSource: MediaSourceInfo|null|undefined): boolean { + return (mediaSource?.TranscodingSubProtocol || mediaSource?.Container) === 'hls'; +} diff --git a/webpack.analyze.js b/webpack.analyze.js new file mode 100644 index 0000000000..3bf6ab053d --- /dev/null +++ b/webpack.analyze.js @@ -0,0 +1,28 @@ +const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const { merge } = require('webpack-merge'); + +const prod = require('./webpack.prod'); + +const smp = new SpeedMeasurePlugin(); + +const config = merge(prod, { + plugins: [ + new BundleAnalyzerPlugin({ + excludeAssets: /-json\..*\.chunk\.js$/ + }) + ] +}); + +const searchPlugin = (name) => config.plugins.findIndex((e) => e.constructor.name === name); + +// NOTE: We need to re-add the mini css plugin to workaround this issue +// https://github.com/stephencookdev/speed-measure-webpack-plugin/issues/167 +const miniCssPluginIndex = searchPlugin('MiniCssExtractPlugin'); +const miniCssPlugin = config.plugins[miniCssPluginIndex]; + +const exportedConfig = smp.wrap(config); + +exportedConfig.plugins[miniCssPluginIndex] = miniCssPlugin; + +module.exports = exportedConfig; diff --git a/webpack.common.js b/webpack.common.js index 59d32fb952..8022569b88 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,6 +1,7 @@ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { DefinePlugin } = require('webpack'); @@ -100,6 +101,11 @@ const config = { to: path.resolve(__dirname, './dist') }; }) + }), + new ForkTsCheckerWebpackPlugin({ + typescript: { + configFile: path.resolve(__dirname, 'tsconfig.json') + } }) ], output: { @@ -110,6 +116,8 @@ const config = { }, optimization: { runtimeChunk: 'single', + removeAvailableModules: false, + removeEmptyChunks: false, splitChunks: { chunks: 'all', maxInitialRequests: Infinity, @@ -216,14 +224,22 @@ const config = { exclude: /node_modules/, use: [ 'worker-loader', - 'ts-loader' + { + loader: 'ts-loader', + options: { + transpileOnly: true + } + } ] }, { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: [{ - loader: 'ts-loader' + loader: 'ts-loader', + options: { + transpileOnly: true + } }] }, /* modules that Babel breaks when transforming to ESM */