diff --git a/.eslintrc.js b/.eslintrc.js index b684293fa..f461a6ceb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { root: true, plugins: [ '@babel', + 'react', 'promise', 'import', 'eslint-comments' @@ -18,11 +19,13 @@ module.exports = { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { - impliedStrict: true + impliedStrict: true, + jsx: true } }, extends: [ 'eslint:recommended', + 'plugin:react/recommended', // 'plugin:promise/recommended', 'plugin:import/errors', 'plugin:eslint-comments/recommended', @@ -36,6 +39,7 @@ module.exports = { 'comma-spacing': ['error'], 'eol-last': ['error'], 'indent': ['error', 4, { 'SwitchCase': 1 }], + 'jsx-quotes': ['error', 'prefer-single'], 'keyword-spacing': ['error'], 'max-statements-per-line': ['error'], 'no-floating-decimal': ['error'], @@ -54,6 +58,11 @@ module.exports = { 'space-infix-ops': 'error', 'yoda': 'error' }, + settings: { + react: { + version: 'detect' + } + }, overrides: [ { files: [ diff --git a/babel.config.js b/babel.config.js index e68d3fd74..e31ea2312 100644 --- a/babel.config.js +++ b/babel.config.js @@ -11,7 +11,8 @@ module.exports = { useBuiltIns: 'usage', corejs: 3 } - ] + ], + '@babel/preset-react' ], plugins: [ '@babel/plugin-proposal-class-properties', diff --git a/package-lock.json b/package-lock.json index 26be43eb9..652a9cc9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -758,6 +758,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz", + "integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, "@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -1260,6 +1269,65 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, + "@babel/plugin-transform-react-display-name": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.2.tgz", + "integrity": "sha512-zCubvP+jjahpnFJvPaHPiGVfuVUjXHhFvJKQdNnsmSsiU9kR/rCZ41jHc++tERD2zV+p7Hr6is+t5b6iWTCqSw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.3.tgz", + "integrity": "sha512-uuxuoUNVhdgYzERiHHFkE4dWoJx+UFVyuAl0aqN8P2/AKFHwqgUC5w2+4/PjpKXJsFgBlYAFXlUmDQ3k3DUkXw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/types": "^7.14.2" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/types": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz", + "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz", + "integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.12.17" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-transform-regenerator": { "version": "7.13.15", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz", @@ -1473,6 +1541,20 @@ "esutils": "^2.0.2" } }, + "@babel/preset-react": { + "version": "7.13.13", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.13.13.tgz", + "integrity": "sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-react-display-name": "^7.12.13", + "@babel/plugin-transform-react-jsx": "^7.13.12", + "@babel/plugin-transform-react-jsx-development": "^7.12.17", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + } + }, "@babel/runtime": { "version": "7.13.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", @@ -2362,6 +2444,18 @@ "es-abstract": "^1.18.0-next.1" } }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2916,6 +3010,11 @@ "version": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "integrity": "sha512-5rjszPzcjFVoDEOarszcbax2WIGT3+fO+W212ZWg9+ylGJgxG1IIcCFjnnBbSdM0lNeIfmMGhhEGovIlr+1yBg==" }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -4609,6 +4708,47 @@ "integrity": "sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==", "dev": true }, + "eslint-plugin-react": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz", + "integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.3", + "object.fromentries": "^2.0.4", + "object.values": "^1.1.3", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "string.prototype.matchall": "^4.0.4" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + } + } + }, "eslint-rule-composer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", @@ -6041,6 +6181,17 @@ "ipaddr.js": "^1.9.0" } }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -6518,8 +6669,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.1", @@ -6594,6 +6744,16 @@ "jquery": ">=1.9.1" } }, + "jsx-ast-utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", + "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", + "dev": true, + "requires": { + "array-includes": "^3.1.2", + "object.assign": "^4.1.2" + } + }, "jszip": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", @@ -6757,6 +6917,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -6897,6 +7062,14 @@ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -7510,8 +7683,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -7593,6 +7765,77 @@ "object-keys": "^1.1.1" } }, + "object.entries": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", + "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + } + } + }, + "object.fromentries": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", + "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "has": "^1.0.3" + } + }, "object.getownpropertydescriptors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", @@ -9267,6 +9510,16 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -9379,6 +9632,30 @@ } } }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "read-file-stdin": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz", @@ -9957,6 +10234,15 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -10181,6 +10467,17 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -10636,6 +10933,70 @@ "strip-ansi": "^6.0.0" } }, + "string.prototype.matchall": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", + "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + } + } + }, "string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", diff --git a/package.json b/package.json index aad63f251..6cc0f8de3 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@babel/plugin-proposal-private-methods": "^7.12.13", "@babel/plugin-transform-modules-umd": "^7.14.0", "@babel/preset-env": "^7.14.1", + "@babel/preset-react": "^7.13.13", "@uupaa/dynamic-import-polyfill": "^1.0.2", "autoprefixer": "^9.8.6", "babel-loader": "^8.2.2", @@ -26,6 +27,7 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-react": "^7.23.2", "expose-loader": "^2.0.0", "file-loader": "^6.2.0", "html-loader": "^1.1.0", @@ -56,6 +58,7 @@ "@fontsource/noto-sans-sc": "^4.2.1", "blurhash": "^1.1.3", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", + "classnames": "^2.3.1", "core-js": "^3.11.2", "date-fns": "^2.21.1", "epubjs": "^0.3.85", @@ -69,10 +72,14 @@ "jstree": "^3.3.11", "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", + "lodash-es": "^4.17.21", "material-design-icons-iconfont": "^6.1.0", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", "pdfjs-dist": "2.6.347", + "prop-types": "^15.7.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "sortablejs": "^1.13.0", diff --git a/src/components/alphaPicker/AlphaPickerComponent.js b/src/components/alphaPicker/AlphaPickerComponent.js new file mode 100644 index 000000000..ae325c24f --- /dev/null +++ b/src/components/alphaPicker/AlphaPickerComponent.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; + +import AlphaPicker from './alphaPicker'; + +// React compatibility wrapper component for alphaPicker.js +const AlphaPickerComponent = ({ onAlphaPicked = () => {} }) => { + const [ alphaPicker, setAlphaPicker ] = useState(null); + const element = useRef(null); + + useEffect(() => { + setAlphaPicker(new AlphaPicker({ + element: element.current, + mode: 'keyboard' + })); + + element.current?.addEventListener('alphavalueclicked', onAlphaPicked); + + return () => { + alphaPicker?.destroy(); + }; + }, []); + + return ( +
+ ); +}; + +AlphaPickerComponent.propTypes = { + onAlphaPicked: PropTypes.func +}; + +export default AlphaPickerComponent; diff --git a/src/components/appRouter.js b/src/components/appRouter.js index cfc04b1fe..6d189a13d 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -11,6 +11,7 @@ import viewManager from './viewManager/viewManager'; import Dashboard from '../scripts/clientUtils'; import ServerConnections from './ServerConnections'; import alert from './alert'; +import reactControllerFactory from './reactControllerFactory'; class AppRouter { allRoutes = []; @@ -341,7 +342,9 @@ class AppRouter { this.sendRouteToViewManager(ctx, next, route, controllerFactory); }; - if (route.controller) { + if (route.pageComponent) { + onInitComplete(reactControllerFactory); + } else if (route.controller) { import('../controllers/' + route.controller).then(onInitComplete); } else { onInitComplete(); @@ -373,6 +376,7 @@ class AppRouter { fullscreen: route.fullscreen, controllerFactory: controllerFactory, options: { + pageComponent: route.pageComponent, supportsThemeMedia: route.supportsThemeMedia || false, enableMediaControl: route.enableMediaControl !== false }, diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js new file mode 100644 index 000000000..93e277988 --- /dev/null +++ b/src/components/pages/SearchPage.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; + +import SearchFields from '../search/SearchFields'; +import SearchResults from '../search/SearchResults'; +import SearchSuggestions from '../search/SearchSuggestions'; +import LiveTVSearchResults from '../search/LiveTVSearchResults'; + +const SearchPage = ({ serverId, parentId, collectionType }) => { + const [ query, setQuery ] = useState(null); + + return ( + <> + + {!query && + + } + + + + ); +}; + +SearchPage.propTypes = { + serverId: PropTypes.string, + parentId: PropTypes.string, + collectionType: PropTypes.string +}; + +export default SearchPage; diff --git a/src/components/reactControllerFactory.js b/src/components/reactControllerFactory.js new file mode 100644 index 000000000..9611b706e --- /dev/null +++ b/src/components/reactControllerFactory.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +export default (view, params, { detail }) => { + if (detail.options?.pageComponent) { + // Fetch and render the page component to the view + import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`) + .then(({ default: component }) => { + ReactDOM.render(React.createElement(component, params), view); + }); + + // Unmount component when view is destroyed + view.addEventListener('viewdestroy', () => { + ReactDOM.unmountComponentAtNode(view); + }); + } +}; diff --git a/src/components/search/LiveTVSearchResults.js b/src/components/search/LiveTVSearchResults.js new file mode 100644 index 000000000..871c86a96 --- /dev/null +++ b/src/components/search/LiveTVSearchResults.js @@ -0,0 +1,191 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; + +import globalize from '../../scripts/globalize'; +import ServerConnections from '../ServerConnections'; +import SearchResultsRow from './SearchResultsRow'; + +const CARD_OPTIONS = { + preferThumb: true, + inheritThumb: false, + showParentTitleOrTitle: true, + showTitle: false, + coverImage: true, + overlayMoreButton: true, + showAirTime: true, + showAirDateTime: true, + showChannelName: true +}; + +/* + * React component to display search result rows for live tv library search + */ +const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { + const [ movies, setMovies ] = useState([]); + const [ episodes, setEpisodes ] = useState([]); + const [ sports, setSports ] = useState([]); + const [ kids, setKids ] = useState([]); + const [ news, setNews ] = useState([]); + const [ programs, setPrograms ] = useState([]); + const [ channels, setChannels ] = useState([]); + + const getDefaultParameters = () => ({ + ParentId: parentId, + searchTerm: query, + Limit: 24, + Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', + Recursive: true, + EnableTotalRecordCount: false, + ImageTypeLimit: 1, + IncludePeople: false, + IncludeMedia: false, + IncludeGenres: false, + IncludeStudios: false, + IncludeArtists: false + }); + + // FIXME: This query does not support Live TV filters + const fetchItems = (apiClient, params = {}) => apiClient?.getItems( + apiClient?.getCurrentUserId(), + { + ...getDefaultParameters(), + IncludeMedia: true, + ...params + } + ); + + const isLiveTV = () => collectionType === 'livetv'; + + useEffect(() => { + // Reset state + setMovies([]); + setEpisodes([]); + setSports([]); + setKids([]); + setNews([]); + setPrograms([]); + setChannels([]); + + if (query && isLiveTV()) { + const apiClient = ServerConnections.getApiClient(serverId); + + // Movies row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: true, + IsSeries: false, + IsSports: false, + IsKids: false, + IsNews: false + }).then(result => setMovies(result.Items)); + // Episodes row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: true, + IsSports: false, + IsKids: false, + IsNews: false + }).then(result => setEpisodes(result.Items)); + // Sports row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: false, + IsSports: true, + IsKids: false, + IsNews: false + }).then(result => setSports(result.Items)); + // Kids row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: false, + IsSports: false, + IsKids: true, + IsNews: false + }).then(result => setKids(result.Items)); + // News row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: false, + IsSports: false, + IsKids: false, + IsNews: true + }).then(result => setNews(result.Items)); + // Programs row + fetchItems(apiClient, { + IncludeItemTypes: 'LiveTvProgram', + IsMovie: false, + IsSeries: false, + IsSports: false, + IsKids: false, + IsNews: false + }).then(result => setPrograms(result.Items)); + // Channels row + fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) + .then(result => setChannels(result.Items)); + } + }, [ query ]); + + return ( +
+ + + + + + + +
+ ); +}; + +LiveTVSearchResults.propTypes = { + serverId: PropTypes.string, + parentId: PropTypes.string, + collectionType: PropTypes.string, + query: PropTypes.string +}; + +export default LiveTVSearchResults; diff --git a/src/components/search/SearchFields.js b/src/components/search/SearchFields.js new file mode 100644 index 000000000..e76f533b1 --- /dev/null +++ b/src/components/search/SearchFields.js @@ -0,0 +1,90 @@ +import debounce from 'lodash-es/debounce'; +import PropTypes from 'prop-types'; +import React, { useEffect, useMemo, useRef } from 'react'; + +import AlphaPicker from '../alphaPicker/AlphaPickerComponent'; +import globalize from '../../scripts/globalize'; + +import 'material-design-icons-iconfont'; + +import '../../elements/emby-input/emby-input'; +import '../../assets/css/flexstyles.scss'; +import './searchfields.scss'; +import layoutManager from '../layoutManager'; +import browser from '../../scripts/browser'; + +// There seems to be some compatibility issues here between +// React and our legacy web components, so we need to inject +// them as an html string for now =/ +const createInputElement = () => ({ + __html: `` +}); + +const normalizeInput = (value = '') => value.trim(); + +const SearchFields = ({ onSearch = () => {} }) => { + const element = useRef(null); + + const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch'); + + const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), []); + + useEffect(() => { + getSearchInput()?.addEventListener('input', e => { + debouncedOnSearch(normalizeInput(e.target?.value)); + }); + getSearchInput()?.focus(); + + return () => { + debouncedOnSearch.cancel(); + }; + }, []); + + const onAlphaPicked = e => { + const value = e.detail.value; + const searchInput = getSearchInput(); + + if (value === 'backspace') { + const currentValue = searchInput.value; + searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : ''; + } else { + searchInput.value += value; + } + + searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); + }; + + return ( +
+
+ +
+
+ {layoutManager.tv && !browser.tv && + + } +
+ ); +}; + +SearchFields.propTypes = { + onSearch: PropTypes.func +}; + +export default SearchFields; diff --git a/src/components/search/SearchResults.js b/src/components/search/SearchResults.js new file mode 100644 index 000000000..3b6322e20 --- /dev/null +++ b/src/components/search/SearchResults.js @@ -0,0 +1,268 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; + +import globalize from '../../scripts/globalize'; +import ServerConnections from '../ServerConnections'; +import SearchResultsRow from './SearchResultsRow'; + +/* + * React component to display search result rows for global search and non-live tv library search + */ +const SearchResults = ({ serverId, parentId, collectionType, query }) => { + const [ movies, setMovies ] = useState([]); + const [ shows, setShows ] = useState([]); + const [ episodes, setEpisodes ] = useState([]); + const [ videos, setVideos ] = useState([]); + const [ programs, setPrograms ] = useState([]); + const [ channels, setChannels ] = useState([]); + const [ playlists, setPlaylists ] = useState([]); + const [ artists, setArtists ] = useState([]); + const [ albums, setAlbums ] = useState([]); + const [ songs, setSongs ] = useState([]); + const [ photoAlbums, setPhotoAlbums ] = useState([]); + const [ photos, setPhotos ] = useState([]); + const [ audioBooks, setAudioBooks ] = useState([]); + const [ books, setBooks ] = useState([]); + const [ people, setPeople ] = useState([]); + + const getDefaultParameters = () => ({ + ParentId: parentId, + searchTerm: query, + Limit: 24, + Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', + Recursive: true, + EnableTotalRecordCount: false, + ImageTypeLimit: 1, + IncludePeople: false, + IncludeMedia: false, + IncludeGenres: false, + IncludeStudios: false, + IncludeArtists: false + }); + + const fetchArtists = (apiClient, params = {}) => apiClient?.getArtists( + apiClient?.getCurrentUserId(), + { + ...getDefaultParameters(), + IncludeArtists: true, + ...params + } + ); + + const fetchItems = (apiClient, params = {}) => apiClient?.getItems( + apiClient?.getCurrentUserId(), + { + ...getDefaultParameters(), + IncludeMedia: true, + ...params + } + ); + + const fetchPeople = (apiClient, params = {}) => apiClient?.getPeople( + apiClient?.getCurrentUserId(), + { + ...getDefaultParameters(), + IncludePeople: true, + ...params + } + ); + + const isMovies = () => collectionType === 'movies'; + + const isMusic = () => collectionType === 'music'; + + const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv'; + + useEffect(() => { + // Reset state + setMovies([]); + setShows([]); + setEpisodes([]); + setVideos([]); + setPrograms([]); + setChannels([]); + setPlaylists([]); + setArtists([]); + setAlbums([]); + setSongs([]); + setPhotoAlbums([]); + setPhotos([]); + setAudioBooks([]); + setBooks([]); + setPeople([]); + + if (query) { + const apiClient = ServerConnections.getApiClient(serverId); + + // Movie libraries + if (!collectionType || isMovies()) { + // Movies row + fetchItems(apiClient, { IncludeItemTypes: 'Movie' }) + .then(result => setMovies(result.Items)); + } + + // TV Show libraries + if (!collectionType || isTVShows()) { + // Shows row + fetchItems(apiClient, { IncludeItemTypes: 'Series' }) + .then(result => setShows(result.Items)); + // Episodes row + fetchItems(apiClient, { IncludeItemTypes: 'Episode' }) + .then(result => setEpisodes(result.Items)); + } + + // People are included for Movies and TV Shows + if (!collectionType || isMovies() || isTVShows()) { + // People row + fetchPeople(apiClient).then(result => setPeople(result.Items)); + } + + // Music libraries + if (!collectionType || isMusic()) { + // Playlists row + fetchItems(apiClient, { IncludeItemTypes: 'Playlist' }) + .then(results => setPlaylists(results.Items)); + // Artists row + fetchArtists(apiClient).then(result => setArtists(result.Items)); + // Albums row + fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' }) + .then(result => setAlbums(result.Items)); + // Songs row + fetchItems(apiClient, { IncludeItemTypes: 'Audio' }) + .then(result => setSongs(result.Items)); + } + + // Other libraries do not support in-library search currently + if (!collectionType) { + // Videos row + fetchItems(apiClient, { + MediaTypes: 'Video', + ExcludeItemTypes: 'Movie,Episode,TvChannel' + }).then(result => setVideos(result.Items)); + // Programs row + fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' }) + .then(result => setPrograms(result.Items)); + // Channels row + fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) + .then(result => setChannels(result.Items)); + // Photo Albums row + fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' }) + .then(results => setPhotoAlbums(results.Items)); + // Photos row + fetchItems(apiClient, { IncludeItemTypes: 'Photo' }) + .then(results => setPhotos(results.Items)); + // Audio Books row + fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' }) + .then(results => setAudioBooks(results.Items)); + // Books row + fetchItems(apiClient, { IncludeItemTypes: 'Book' }) + .then(results => setBooks(results.Items)); + } + } + }, [ query ]); + + return ( +
+ + + + + + + + + + + + + + + +
+ ); +}; + +SearchResults.propTypes = { + serverId: PropTypes.string, + parentId: PropTypes.string, + collectionType: PropTypes.string, + query: PropTypes.string +}; + +export default SearchResults; diff --git a/src/components/search/SearchResultsRow.js b/src/components/search/SearchResultsRow.js new file mode 100644 index 000000000..3667d8486 --- /dev/null +++ b/src/components/search/SearchResultsRow.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; + +import cardBuilder from '../cardbuilder/cardBuilder'; + +import '../../elements/emby-scroller/emby-scroller'; +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +// There seems to be some compatibility issues here between +// React and our legacy web components, so we need to inject +// them as an html string for now =/ +const createScroller = ({ title = '' }) => ({ + __html: `

${title}

+
+
+
` +}); + +const SearchResultsRow = ({ title, items = [], cardOptions = {} }) => { + const element = useRef(null); + + useEffect(() => { + cardBuilder.buildCards(items, { + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current, + shape: 'autooverflow', + scalable: true, + showTitle: true, + overlayText: false, + centerText: true, + allowBottomPadding: false, + ...cardOptions + }); + }, [ items ]); + + return ( +
+ ); +}; + +SearchResultsRow.propTypes = { + title: PropTypes.string, + items: PropTypes.array, + cardOptions: PropTypes.object +}; + +export default SearchResultsRow; diff --git a/src/components/search/SearchSuggestions.js b/src/components/search/SearchSuggestions.js new file mode 100644 index 000000000..fbfd2de8a --- /dev/null +++ b/src/components/search/SearchSuggestions.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; + +import { appRouter } from '../appRouter'; +import globalize from '../../scripts/globalize'; +import ServerConnections from '../ServerConnections'; + +import '../../elements/emby-button/emby-button'; + +// There seems to be some compatibility issues here between +// React and our legacy web components, so we need to inject +// them as an html string for now =/ +const createSuggestionLink = ({name, href}) => ({ + __html: `${name}` +}); + +const SearchSuggestions = ({ serverId, parentId }) => { + const [ suggestions, setSuggestions ] = useState([]); + + useEffect(() => { + const apiClient = ServerConnections.getApiClient(serverId); + + apiClient.getItems(apiClient.getCurrentUserId(), { + SortBy: 'IsFavoriteOrLiked,Random', + IncludeItemTypes: 'Movie,Series,MusicArtist', + Limit: 20, + Recursive: true, + ImageTypeLimit: 0, + EnableImages: false, + ParentId: parentId, + EnableTotalRecordCount: false + }).then(result => setSuggestions(result.Items)); + }, []); + + return ( +
+
+

+ {globalize.translate('Suggestions')} +

+
+ +
+ {suggestions.map(item => ( +
+ ))} +
+
+ ); +}; + +SearchSuggestions.propTypes = { + parentId: PropTypes.string, + serverId: PropTypes.string +}; + +export default SearchSuggestions; diff --git a/src/components/search/searchfields.js b/src/components/search/searchfields.js deleted file mode 100644 index 6a8be15be..000000000 --- a/src/components/search/searchfields.js +++ /dev/null @@ -1,115 +0,0 @@ -import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; -import { Events } from 'jellyfin-apiclient'; -import browser from '../../scripts/browser'; -import AlphaPicker from '../alphaPicker/alphaPicker'; -import '../../elements/emby-input/emby-input'; -import '../../assets/css/flexstyles.scss'; -import 'material-design-icons-iconfont'; -import './searchfields.scss'; -import template from './searchfields.template.html'; - -/* eslint-disable indent */ - - function onSearchTimeout() { - const instance = this; - let value = instance.nextSearchValue; - - value = (value || '').trim(); - Events.trigger(instance, 'search', [value]); - } - - function triggerSearch(instance, value) { - if (instance.searchTimeout) { - clearTimeout(instance.searchTimeout); - } - - instance.nextSearchValue = value; - instance.searchTimeout = setTimeout(onSearchTimeout.bind(instance), 400); - } - - function onAlphaValueClicked(e) { - const value = e.detail.value; - const searchFieldsInstance = this; - - const txtSearch = searchFieldsInstance.options.element.querySelector('.searchfields-txtSearch'); - - if (value === 'backspace') { - const val = txtSearch.value; - txtSearch.value = val.length ? val.substring(0, val.length - 1) : ''; - } else { - txtSearch.value += value; - } - - txtSearch.dispatchEvent(new CustomEvent('input', { - bubbles: true - })); - } - - function initAlphaPicker(alphaPickerElement, instance) { - instance.alphaPicker = new AlphaPicker({ - element: alphaPickerElement, - mode: 'keyboard' - }); - - alphaPickerElement.addEventListener('alphavalueclicked', onAlphaValueClicked.bind(instance)); - } - - function onSearchInput(e) { - const value = e.target.value; - const searchFieldsInstance = this; - triggerSearch(searchFieldsInstance, value); - } - - function embed(elem, instance) { - elem.innerHTML = globalize.translateHtml(template, 'core'); - - elem.classList.add('searchFields'); - - const txtSearch = elem.querySelector('.searchfields-txtSearch'); - - if (layoutManager.tv && !browser.tv) { - const alphaPickerElement = elem.querySelector('.alphaPicker'); - - elem.querySelector('.alphaPicker').classList.remove('hide'); - initAlphaPicker(alphaPickerElement, instance); - } - - txtSearch.addEventListener('input', onSearchInput.bind(instance)); - - instance.focus(); - } - -class SearchFields { - constructor(options) { - this.options = options; - embed(options.element, this); - } - focus() { - this.options.element.querySelector('.searchfields-txtSearch').focus(); - } - destroy() { - const options = this.options; - if (options) { - options.element.classList.remove('searchFields'); - } - this.options = null; - - const alphaPicker = this.alphaPicker; - if (alphaPicker) { - alphaPicker.destroy(); - } - this.alphaPicker = null; - - const searchTimeout = this.searchTimeout; - if (searchTimeout) { - clearTimeout(searchTimeout); - } - this.searchTimeout = null; - this.nextSearchValue = null; - } -} - -export default SearchFields; - -/* eslint-enable indent */ diff --git a/src/components/search/searchfields.template.html b/src/components/search/searchfields.template.html deleted file mode 100644 index 2ba21492b..000000000 --- a/src/components/search/searchfields.template.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
- -
-
-
diff --git a/src/components/search/searchresults.js b/src/components/search/searchresults.js deleted file mode 100644 index 78d222623..000000000 --- a/src/components/search/searchresults.js +++ /dev/null @@ -1,624 +0,0 @@ -import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; -import cardBuilder from '../cardbuilder/cardBuilder'; -import { appRouter } from '../appRouter'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-button/emby-button'; -import ServerConnections from '../ServerConnections'; -import template from './searchresults.template.html'; - -/* eslint-disable indent */ - - function loadSuggestions(instance, context, apiClient) { - const options = { - - SortBy: 'IsFavoriteOrLiked,Random', - IncludeItemTypes: 'Movie,Series,MusicArtist', - Limit: 20, - Recursive: true, - ImageTypeLimit: 0, - EnableImages: false, - ParentId: instance.options.parentId, - EnableTotalRecordCount: false - }; - - apiClient.getItems(apiClient.getCurrentUserId(), options).then(function (result) { - if (instance.mode !== 'suggestions') { - result.Items = []; - } - - const html = result.Items.map(function (i) { - const href = appRouter.getRouteUrl(i); - - let itemHtml = ''; - return itemHtml; - }).join(''); - - const searchSuggestions = context.querySelector('.searchSuggestions'); - searchSuggestions.querySelector('.searchSuggestionsList').innerHTML = html; - - if (result.Items.length) { - searchSuggestions.classList.remove('hide'); - } - }); - } - - function getSearchHints(instance, apiClient, query) { - if (!query.searchTerm) { - return Promise.resolve({ - SearchHints: [] - }); - } - - let allowSearch = true; - - const queryIncludeItemTypes = query.IncludeItemTypes; - - if (instance.options.collectionType === 'tvshows') { - if (query.IncludeArtists) { - allowSearch = false; - } else if (queryIncludeItemTypes === 'Movie' || - queryIncludeItemTypes === 'LiveTvProgram' || - queryIncludeItemTypes === 'MusicAlbum' || - queryIncludeItemTypes === 'Audio' || - queryIncludeItemTypes === 'Book' || - queryIncludeItemTypes === 'AudioBook' || - queryIncludeItemTypes === 'Playlist' || - queryIncludeItemTypes === 'PhotoAlbum' || - query.MediaTypes === 'Video' || - query.MediaTypes === 'Photo') { - allowSearch = false; - } - } else if (instance.options.collectionType === 'movies') { - if (query.IncludeArtists) { - allowSearch = false; - } else if (queryIncludeItemTypes === 'Series' || - queryIncludeItemTypes === 'Episode' || - queryIncludeItemTypes === 'LiveTvProgram' || - queryIncludeItemTypes === 'MusicAlbum' || - queryIncludeItemTypes === 'Audio' || - queryIncludeItemTypes === 'Book' || - queryIncludeItemTypes === 'AudioBook' || - queryIncludeItemTypes === 'Playlist' || - queryIncludeItemTypes === 'PhotoAlbum' || - query.MediaTypes === 'Video' || - query.MediaTypes === 'Photo') { - allowSearch = false; - } - } else if (instance.options.collectionType === 'music') { - if (query.People) { - allowSearch = false; - } else if (queryIncludeItemTypes === 'Series' || - queryIncludeItemTypes === 'Episode' || - queryIncludeItemTypes === 'LiveTvProgram' || - queryIncludeItemTypes === 'Movie') { - allowSearch = false; - } - } else if (instance.options.collectionType === 'livetv') { - if (query.IncludeArtists || query.IncludePeople) { - allowSearch = false; - } else if (queryIncludeItemTypes === 'Series' || - queryIncludeItemTypes === 'Episode' || - queryIncludeItemTypes === 'MusicAlbum' || - queryIncludeItemTypes === 'Audio' || - queryIncludeItemTypes === 'Book' || - queryIncludeItemTypes === 'AudioBook' || - queryIncludeItemTypes === 'PhotoAlbum' || - queryIncludeItemTypes === 'Movie' || - query.MediaTypes === 'Video' || - query.MediaTypes === 'Photo') { - allowSearch = false; - } - } - if (queryIncludeItemTypes === 'NullType') { - allowSearch = false; - } - - if (!allowSearch) { - return Promise.resolve({ - SearchHints: [] - }); - } - - // Convert the search hint query to a regular item query - if (apiClient.isMinServerVersion('3.4.1.31')) { - query.Fields = 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount'; - query.Recursive = true; - query.EnableTotalRecordCount = false; - query.ImageTypeLimit = 1; - - let methodName = 'getItems'; - - if (!query.IncludeMedia) { - if (query.IncludePeople) { - methodName = 'getPeople'; - } else if (query.IncludeArtists) { - methodName = 'getArtists'; - } - } - - return apiClient[methodName](apiClient.getCurrentUserId(), query); - } - - query.UserId = apiClient.getCurrentUserId(); - - return apiClient.getSearchHints(query); - } - - function search(instance, apiClient, context, value) { - if (value || layoutManager.tv) { - instance.mode = 'search'; - context.querySelector('.searchSuggestions').classList.add('hide'); - } else { - instance.mode = 'suggestions'; - loadSuggestions(instance, context, apiClient); - } - - if (instance.options.collectionType === 'livetv') { - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'LiveTvProgram', - IsMovie: true, - IsKids: false, - IsNews: false - - }, context, '.movieResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowPortrait' : 'portrait'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - }); - } else { - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Movie' - - }, context, '.movieResults', { - - showTitle: true, - overlayText: false, - centerText: true, - showYear: true - }); - } - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Series' - - }, context, '.seriesResults', { - - showTitle: true, - overlayText: false, - centerText: true, - showYear: true - }); - - if (instance.options.collectionType === 'livetv') { - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'LiveTvProgram', - IsSeries: true, - IsSports: false, - IsKids: false, - IsNews: false - - }, context, '.episodeResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - }); - } else { - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Episode' - - }, context, '.episodeResults', { - - coverImage: true, - showTitle: true, - showParentTitle: true - }); - } - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - // NullType to hide - IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType', - IsSports: true - - }, context, '.sportsResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - // NullType to hide - IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType', - IsKids: true - - }, context, '.kidsResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - // NullType to hide - IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType', - IsNews: true - - }, context, '.newsResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'LiveTvProgram', - IsMovie: instance.options.collectionType === 'livetv' ? false : null, - IsSeries: instance.options.collectionType === 'livetv' ? false : null, - IsSports: instance.options.collectionType === 'livetv' ? false : null, - IsKids: instance.options.collectionType === 'livetv' ? false : null, - IsNews: instance.options.collectionType === 'livetv' ? false : null - - }, context, '.programResults', { - - preferThumb: true, - inheritThumb: false, - shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'), - showParentTitleOrTitle: true, - showTitle: false, - centerText: true, - coverImage: true, - overlayText: false, - overlayMoreButton: true, - showAirTime: true, - showAirDateTime: true, - showChannelName: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - MediaTypes: 'Video', - ExcludeItemTypes: 'Movie,Episode' - - }, context, '.videoResults', { - - showParentTitle: true, - showTitle: true, - overlayText: false, - centerText: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: true, - IncludeMedia: false, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false - - }, context, '.peopleResults', { - - coverImage: true, - showTitle: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: false, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: true - - }, context, '.artistResults', { - coverImage: true, - showTitle: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'MusicAlbum' - - }, context, '.albumResults', { - - showParentTitle: true, - showTitle: true, - overlayText: false, - centerText: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Audio' - - }, context, '.songResults', { - - showParentTitle: true, - showTitle: true, - overlayText: false, - centerText: true, - overlayPlayButton: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - MediaTypes: 'Photo' - - }, context, '.photoResults', { - - showParentTitle: false, - showTitle: true, - overlayText: false, - centerText: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'PhotoAlbum' - - }, context, '.photoAlbumResults', { - - showTitle: true, - overlayText: false, - centerText: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Book' - - }, context, '.bookResults', { - - showTitle: true, - overlayText: false, - centerText: true - - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'AudioBook' - - }, context, '.audioBookResults', { - - showTitle: true, - overlayText: false, - centerText: true - }); - - searchType(instance, apiClient, { - searchTerm: value, - IncludePeople: false, - IncludeMedia: true, - IncludeGenres: false, - IncludeStudios: false, - IncludeArtists: false, - IncludeItemTypes: 'Playlist' - - }, context, '.playlistResults', { - - showTitle: true, - overlayText: false, - centerText: true - }); - } - - function searchType(instance, apiClient, query, context, section, cardOptions) { - query.Limit = enableScrollX() ? 24 : 16; - query.ParentId = instance.options.parentId; - - getSearchHints(instance, apiClient, query).then(function (result) { - populateResults(result, context, section, cardOptions); - }); - } - - function populateResults(result, context, section, cardOptions) { - section = context.querySelector(section); - - const items = result.Items || result.SearchHints; - - const itemsContainer = section.querySelector('.itemsContainer'); - - cardBuilder.buildCards(items, Object.assign({ - - itemsContainer: itemsContainer, - parentContainer: section, - shape: enableScrollX() ? 'autooverflow' : 'auto', - scalable: true, - overlayText: false, - centerText: true, - allowBottomPadding: !enableScrollX() - - }, cardOptions || {})); - } - - function enableScrollX() { - return true; - } - - function replaceAll(originalString, strReplace, strWith) { - const reg = new RegExp(strReplace, 'ig'); - return originalString.replace(reg, strWith); - } - - function embed(elem, instance) { - let workingTemplate = template; - if (!enableScrollX()) { - workingTemplate = replaceAll(workingTemplate, 'data-horizontal="true"', 'data-horizontal="false"'); - workingTemplate = replaceAll(workingTemplate, 'itemsContainer scrollSlider', 'itemsContainer scrollSlider vertical-wrap'); - } - - const html = globalize.translateHtml(workingTemplate, 'core'); - - elem.innerHTML = html; - - elem.classList.add('searchResults'); - instance.search(''); - } - -class SearchResults { - constructor(options) { - this.options = options; - embed(options.element, this); - } - search(value) { - const apiClient = ServerConnections.getApiClient(this.options.serverId); - - search(this, apiClient, this.options.element, value); - } - destroy() { - const options = this.options; - if (options) { - options.element.classList.remove('searchFields'); - } - this.options = null; - } -} - -export default SearchResults; - -/* eslint-enable indent */ diff --git a/src/components/search/searchresults.template.html b/src/components/search/searchresults.template.html deleted file mode 100644 index 1deecaca6..000000000 --- a/src/components/search/searchresults.template.html +++ /dev/null @@ -1,145 +0,0 @@ -
- -
-

${Suggestions}

-
- -
-
-
- -
-

${Movies}

- -
-
-
-
- -
-

${Shows}

- -
-
-
-
- -
-

${Episodes}

- -
-
-
-
- -
-

${Sports}

- -
-
-
-
- -
-

${Kids}

- -
-
-
-
- -
-

${News}

- -
-
-
-
- -
-

${Programs}

- -
-
-
-
- -
-

${Videos}

- -
-
-
-
- -
-

${Playlists}

- -
-
-
-
- -
-

${Artists}

- -
-
-
-
- -
-

${Albums}

- -
-
-
-
- -
-

${Songs}

- -
-
-
-
- -
-

${HeaderPhotoAlbums}

- -
-
-
-
- -
-

${Photos}

- -
-
-
-
- -
-

${HeaderAudioBooks}

- -
-
-
-
- -
-

${Books}

- -
-
-
-
- -
-

${People}

- -
-
-
-
diff --git a/src/components/viewManager/viewManager.js b/src/components/viewManager/viewManager.js index 22faa3a3b..501bd928a 100644 --- a/src/components/viewManager/viewManager.js +++ b/src/components/viewManager/viewManager.js @@ -21,9 +21,9 @@ viewContainer.setOnBeforeChange(function (newView, isRestored, options) { newView.initComplete = true; if (typeof options.controllerFactory === 'function') { - new options.controllerFactory(newView, eventDetail.detail.params); + new options.controllerFactory(newView, eventDetail.detail.params, eventDetail); } else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') { - new options.controllerFactory.default(newView, eventDetail.detail.params); + new options.controllerFactory.default(newView, eventDetail.detail.params, eventDetail); } if (!options.controllerFactory || dispatchPageEvents) { diff --git a/src/controllers/search.html b/src/controllers/search.html index b0277aa7e..e6fa92c0f 100644 --- a/src/controllers/search.html +++ b/src/controllers/search.html @@ -1,4 +1,2 @@
-
-
diff --git a/src/controllers/searchpage.js b/src/controllers/searchpage.js deleted file mode 100644 index b96c6f4b1..000000000 --- a/src/controllers/searchpage.js +++ /dev/null @@ -1,36 +0,0 @@ -import SearchFields from '../components/search/searchfields'; -import SearchResults from '../components/search/searchresults'; -import { Events } from 'jellyfin-apiclient'; - -export default function (view, params) { - function onSearch(e, value) { - self.searchResults.search(value); - } - - const self = this; - view.addEventListener('viewshow', function () { - if (!self.searchFields) { - self.searchFields = new SearchFields({ - element: view.querySelector('.searchFields') - }); - self.searchResults = new SearchResults({ - element: view.querySelector('.searchResults'), - serverId: params.serverId || ApiClient.serverId(), - parentId: params.parentId, - collectionType: params.collectionType - }); - Events.on(self.searchFields, 'search', onSearch); - } - }); - view.addEventListener('viewdestroy', function () { - if (self.searchFields) { - self.searchFields.destroy(); - self.searchFields = null; - } - - if (self.searchResults) { - self.searchResults.destroy(); - self.searchResults = null; - } - }); -} diff --git a/src/scripts/routes.js b/src/scripts/routes.js index f9968a1fd..388c2a83e 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -304,7 +304,7 @@ import { appRouter } from '../components/appRouter'; defineRoute({ alias: '/search.html', path: 'search.html', - controller: 'searchpage' + pageComponent: 'SearchPage' }); defineRoute({