From 4aa203c0c6ca0a37ba9de4164b1f4eb69837bfd1 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 27 May 2021 11:30:03 -0400 Subject: [PATCH 01/16] Add basic react support --- .eslintrc.js | 3 +- babel.config.js | 3 +- package-lock.json | 124 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 3 ++ 4 files changed, 127 insertions(+), 6 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b684293fa..7a452bdee 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,8 @@ module.exports = { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { - impliedStrict: true + impliedStrict: true, + jsx: true } }, extends: [ 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..12dc7959d 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", @@ -6518,8 +6600,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", @@ -6897,6 +6978,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 +7599,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", @@ -9379,6 +9467,25 @@ } } }, + "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" + } + }, "read-file-stdin": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz", @@ -9957,6 +10064,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", diff --git a/package.json b/package.json index aad63f251..11c1eef8c 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", @@ -73,6 +74,8 @@ "native-promise-only": "^0.8.0-a", "page": "^1.11.6", "pdfjs-dist": "2.6.347", + "react": "^17.0.2", + "react-dom": "^17.0.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "sortablejs": "^1.13.0", From eb605615d1d1403e9f92883cb50bc8dd1edc7d9c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 27 May 2021 15:34:27 -0400 Subject: [PATCH 02/16] Add router level support for rendering react componenent pages --- src/components/appRouter.js | 6 +++++- src/components/pages/TestPage.js | 7 +++++++ src/components/reactControllerFactory.js | 13 +++++++++++++ src/components/viewManager/viewManager.js | 4 ++-- src/controllers/reactTest.html | 1 + src/scripts/routes.js | 6 ++++++ 6 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/components/pages/TestPage.js create mode 100644 src/components/reactControllerFactory.js create mode 100644 src/controllers/reactTest.html 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/TestPage.js b/src/components/pages/TestPage.js new file mode 100644 index 000000000..2d4209161 --- /dev/null +++ b/src/components/pages/TestPage.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const TestPage = () => ( +

Hello from React!

+); + +export default TestPage; diff --git a/src/components/reactControllerFactory.js b/src/components/reactControllerFactory.js new file mode 100644 index 000000000..22aa48b38 --- /dev/null +++ b/src/components/reactControllerFactory.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +// TODO: Probably need to rehydrate on view restores + +export default (view, params, { detail }) => { + if (detail.options?.pageComponent) { + import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`) + .then(({ default: component }) => { + ReactDOM.render(React.createElement(component), view); + }); + } +}; 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/reactTest.html b/src/controllers/reactTest.html new file mode 100644 index 000000000..a0cbc97a9 --- /dev/null +++ b/src/controllers/reactTest.html @@ -0,0 +1 @@ +
diff --git a/src/scripts/routes.js b/src/scripts/routes.js index f9968a1fd..7b78dff1e 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -24,6 +24,12 @@ import { appRouter } from '../components/appRouter'; appRouter.addRoute(path, newRoute); } + defineRoute({ + alias: '/reactTest.html', + path: 'reactTest.html', + pageComponent: 'TestPage' + }); + defineRoute({ alias: '/addserver.html', path: 'session/addServer/index.html', From c7296753affe6e4d8c859fd6d01d4fff469939bf Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 12:31:31 -0400 Subject: [PATCH 03/16] Add recommended eslint rules for react --- .eslintrc.js | 8 ++ package-lock.json | 237 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 246 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 7a452bdee..f461a6ceb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { root: true, plugins: [ '@babel', + 'react', 'promise', 'import', 'eslint-comments' @@ -24,6 +25,7 @@ module.exports = { }, extends: [ 'eslint:recommended', + 'plugin:react/recommended', // 'plugin:promise/recommended', 'plugin:import/errors', 'plugin:eslint-comments/recommended', @@ -37,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'], @@ -55,6 +58,11 @@ module.exports = { 'space-infix-ops': 'error', 'yoda': 'error' }, + settings: { + react: { + version: 'detect' + } + }, overrides: [ { files: [ diff --git a/package-lock.json b/package-lock.json index 12dc7959d..0b1115426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2444,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", @@ -4691,6 +4703,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", @@ -6123,6 +6176,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", @@ -6675,6 +6739,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", @@ -7681,6 +7755,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", @@ -9355,6 +9500,17 @@ "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==", + "dev": true, + "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", @@ -9486,6 +9642,12 @@ "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==", + "dev": true + }, "read-file-stdin": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz", @@ -10297,6 +10459,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", @@ -10752,6 +10925,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 11c1eef8c..4670c742d 100644 --- a/package.json +++ b/package.json @@ -27,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", From 903b656f7fecceeac615f64d415732253236462a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 13:38:28 -0400 Subject: [PATCH 04/16] Convert search page to react --- package-lock.json | 4 +- package.json | 1 + src/components/pages/SearchPage.js | 66 ++++++++++++++++++++++++ src/components/reactControllerFactory.js | 2 +- src/controllers/search.html | 2 - src/controllers/searchpage.js | 36 ------------- src/scripts/routes.js | 2 +- 7 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 src/components/pages/SearchPage.js delete mode 100644 src/controllers/searchpage.js diff --git a/package-lock.json b/package-lock.json index 0b1115426..70e88ee5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9504,7 +9504,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9645,8 +9644,7 @@ "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==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "read-file-stdin": { "version": "0.2.1", diff --git a/package.json b/package.json index 4670c742d..6cac8d4d5 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "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", diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js new file mode 100644 index 000000000..8bd9fa0ba --- /dev/null +++ b/src/components/pages/SearchPage.js @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; +import { Events } from 'jellyfin-apiclient'; + +import SearchFields from '../search/searchfields'; +import SearchResults from '../search/searchresults'; + +const SearchPage = ({ serverId, parentId, collectionType }) => { + const [ searchFields, setSearchFields ] = useState(null); + const searchFieldsContainer = useRef(null); + const [ searchResults, setSearchResults ] = useState(null); + const searchResultsContainer = useRef(null); + + useEffect(() => { + if (!searchFields) { + setSearchFields( + new SearchFields({ + element: searchFieldsContainer.current + }) + ); + + setSearchResults( + new SearchResults({ + element: searchResultsContainer.current, + serverId: serverId || ApiClient.serverId(), + parentId, + collectionType + }) + ); + } + + return () => { + searchFields?.destroy(); + searchResults?.destroy(); + }; + }, []); + + useEffect(() => { + if (searchFields) { + Events.on(searchFields, 'search', (e, value) => { + searchResults.search(value); + }); + } + }, [ searchFields ]); + + return ( + <> +
+
+ + ); +}; + +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 index 22aa48b38..5e2d46a95 100644 --- a/src/components/reactControllerFactory.js +++ b/src/components/reactControllerFactory.js @@ -7,7 +7,7 @@ export default (view, params, { detail }) => { if (detail.options?.pageComponent) { import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`) .then(({ default: component }) => { - ReactDOM.render(React.createElement(component), view); + ReactDOM.render(React.createElement(component, params), view); }); } }; 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 7b78dff1e..102d7f58d 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -310,7 +310,7 @@ import { appRouter } from '../components/appRouter'; defineRoute({ alias: '/search.html', path: 'search.html', - controller: 'searchpage' + pageComponent: 'SearchPage' }); defineRoute({ From b9dacecf2258228febd265449269a7d30fa3447c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 15:06:05 -0400 Subject: [PATCH 05/16] Fix indentation of searchfields --- src/components/search/searchfields.js | 114 +++++++++++++------------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/src/components/search/searchfields.js b/src/components/search/searchfields.js index 6a8be15be..1844ad811 100644 --- a/src/components/search/searchfields.js +++ b/src/components/search/searchfields.js @@ -9,76 +9,74 @@ 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; - function onSearchTimeout() { - const instance = this; - let value = instance.nextSearchValue; + value = (value || '').trim(); + Events.trigger(instance, 'search', [value]); +} - value = (value || '').trim(); - Events.trigger(instance, 'search', [value]); +function triggerSearch(instance, value) { + if (instance.searchTimeout) { + clearTimeout(instance.searchTimeout); } - function triggerSearch(instance, value) { - if (instance.searchTimeout) { - clearTimeout(instance.searchTimeout); - } + instance.nextSearchValue = value; + instance.searchTimeout = setTimeout(onSearchTimeout.bind(instance), 400); +} - 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; } - function onAlphaValueClicked(e) { - const value = e.detail.value; - const searchFieldsInstance = this; + txtSearch.dispatchEvent(new CustomEvent('input', { + bubbles: true + })); +} - const txtSearch = searchFieldsInstance.options.element.querySelector('.searchfields-txtSearch'); +function initAlphaPicker(alphaPickerElement, instance) { + instance.alphaPicker = new AlphaPicker({ + element: alphaPickerElement, + mode: 'keyboard' + }); - if (value === 'backspace') { - const val = txtSearch.value; - txtSearch.value = val.length ? val.substring(0, val.length - 1) : ''; - } else { - txtSearch.value += value; - } + alphaPickerElement.addEventListener('alphavalueclicked', onAlphaValueClicked.bind(instance)); +} - txtSearch.dispatchEvent(new CustomEvent('input', { - bubbles: true - })); +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); } - function initAlphaPicker(alphaPickerElement, instance) { - instance.alphaPicker = new AlphaPicker({ - element: alphaPickerElement, - mode: 'keyboard' - }); + txtSearch.addEventListener('input', onSearchInput.bind(instance)); - 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(); - } + instance.focus(); +} class SearchFields { constructor(options) { @@ -111,5 +109,3 @@ class SearchFields { } export default SearchFields; - -/* eslint-enable indent */ From 436a59b56bf53cd7be2a4778573377a5b04b9d5d Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 15:58:41 -0400 Subject: [PATCH 06/16] Cleanup SearchPage implementation --- src/components/pages/SearchPage.js | 57 ++++--------------- .../search/SearchFieldsComponent.js | 41 +++++++++++++ .../search/SearchResultsComponent.js | 44 ++++++++++++++ 3 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 src/components/search/SearchFieldsComponent.js create mode 100644 src/components/search/SearchResultsComponent.js diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js index 8bd9fa0ba..8fa2c69dd 100644 --- a/src/components/pages/SearchPage.js +++ b/src/components/pages/SearchPage.js @@ -1,57 +1,22 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; -import { Events } from 'jellyfin-apiclient'; +import React, { useState } from 'react'; -import SearchFields from '../search/searchfields'; -import SearchResults from '../search/searchresults'; +import SearchFieldsComponent from '../search/SearchFieldsComponent'; +import SearchResultsComponent from '../search/SearchResultsComponent'; const SearchPage = ({ serverId, parentId, collectionType }) => { - const [ searchFields, setSearchFields ] = useState(null); - const searchFieldsContainer = useRef(null); - const [ searchResults, setSearchResults ] = useState(null); - const searchResultsContainer = useRef(null); - - useEffect(() => { - if (!searchFields) { - setSearchFields( - new SearchFields({ - element: searchFieldsContainer.current - }) - ); - - setSearchResults( - new SearchResults({ - element: searchResultsContainer.current, - serverId: serverId || ApiClient.serverId(), - parentId, - collectionType - }) - ); - } - - return () => { - searchFields?.destroy(); - searchResults?.destroy(); - }; - }, []); - - useEffect(() => { - if (searchFields) { - Events.on(searchFields, 'search', (e, value) => { - searchResults.search(value); - }); - } - }, [ searchFields ]); + const [ query, setQuery ] = useState(null); return ( <> -
-
); diff --git a/src/components/search/SearchFieldsComponent.js b/src/components/search/SearchFieldsComponent.js new file mode 100644 index 000000000..10526fa16 --- /dev/null +++ b/src/components/search/SearchFieldsComponent.js @@ -0,0 +1,41 @@ +import { Events } from 'jellyfin-apiclient'; +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; + +import SearchFields from './searchfields'; + +const SearchFieldsComponent = ({ onSearch = () => {} }) => { + const [ searchFields, setSearchFields ] = useState(null); + const searchFieldsElement = useRef(null); + + useEffect(() => { + setSearchFields( + new SearchFields({ element: searchFieldsElement.current }) + ); + + return () => { + searchFields?.destroy(); + }; + }, []); + + useEffect(() => { + if (searchFields) { + Events.on(searchFields, 'search', (e, value) => { + onSearch(value); + }); + } + }, [ searchFields ]); + + return ( +
+ ); +}; + +SearchFieldsComponent.propTypes = { + onSearch: PropTypes.func +}; + +export default SearchFieldsComponent; diff --git a/src/components/search/SearchResultsComponent.js b/src/components/search/SearchResultsComponent.js new file mode 100644 index 000000000..925d74987 --- /dev/null +++ b/src/components/search/SearchResultsComponent.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; + +import SearchResults from './searchresults'; + +const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) => { + const [ searchResults, setSearchResults ] = useState(null); + const searchResultsElement = useRef(null); + + useEffect(() => { + setSearchResults( + new SearchResults({ + element: searchResultsElement.current, + serverId: serverId || ApiClient.serverId(), + parentId, + collectionType + }) + ); + + return () => { + searchResults?.destroy(); + }; + }, []); + + useEffect(() => { + searchResults?.search(query); + }, [ query ]); + + return ( +
+ ); +}; + +SearchResultsComponent.propTypes = { + serverId: PropTypes.string, + parentId: PropTypes.string, + collectionType: PropTypes.string, + query: PropTypes.string +}; + +export default SearchResultsComponent; From 9a520b6c2943119840c716425afad7f05888bd76 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 15:59:34 -0400 Subject: [PATCH 07/16] Fix indentation of searchresults --- src/components/search/searchresults.js | 1050 ++++++++++++------------ 1 file changed, 523 insertions(+), 527 deletions(-) diff --git a/src/components/search/searchresults.js b/src/components/search/searchresults.js index 78d222623..f34a8292b 100644 --- a/src/components/search/searchresults.js +++ b/src/components/search/searchresults.js @@ -8,350 +8,154 @@ 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 = { - 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 + }; - 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 = []; + } - 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); - const html = result.Items.map(function (i) { - const href = appRouter.getRouteUrl(i); + let itemHtml = ''; + return itemHtml; + }).join(''); - let itemHtml = ''; - return itemHtml; - }).join(''); + const searchSuggestions = context.querySelector('.searchSuggestions'); + searchSuggestions.querySelector('.searchSuggestionsList').innerHTML = html; - const searchSuggestions = context.querySelector('.searchSuggestions'); - searchSuggestions.querySelector('.searchSuggestionsList').innerHTML = html; + if (result.Items.length) { + searchSuggestions.classList.remove('hide'); + } + }); +} - if (result.Items.length) { - searchSuggestions.classList.remove('hide'); - } +function getSearchHints(instance, apiClient, query) { + if (!query.searchTerm) { + return Promise.resolve({ + SearchHints: [] }); } - function getSearchHints(instance, apiClient, query) { - if (!query.searchTerm) { - return Promise.resolve({ - SearchHints: [] - }); - } + let allowSearch = true; - let allowSearch = true; + const queryIncludeItemTypes = query.IncludeItemTypes; - 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') { + 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; } - - if (!allowSearch) { - return Promise.resolve({ - SearchHints: [] - }); + } 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; } - - // 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); + } else if (instance.options.collectionType === 'music') { + if (query.People) { + allowSearch = false; + } else if (queryIncludeItemTypes === 'Series' || + queryIncludeItemTypes === 'Episode' || + queryIncludeItemTypes === 'LiveTvProgram' || + queryIncludeItemTypes === 'Movie') { + allowSearch = false; } - - query.UserId = apiClient.getCurrentUserId(); - - return apiClient.getSearchHints(query); + } 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; } - 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 (!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'; + } } - 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 + return apiClient[methodName](apiClient.getCurrentUserId(), query); + } - }, context, '.movieResults', { + query.UserId = apiClient.getCurrentUserId(); - 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' + return apiClient.getSearchHints(query); +} - }, 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 - - }); +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, @@ -360,13 +164,76 @@ import template from './searchresults.template.html'; 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 + IsMovie: true, + IsKids: false, + IsNews: false - }, context, '.programResults', { + }, 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, @@ -380,9 +247,8 @@ import template from './searchresults.template.html'; showAirTime: true, showAirDateTime: true, showChannelName: true - }); - + } else { searchType(instance, apiClient, { searchTerm: value, IncludePeople: false, @@ -390,215 +256,347 @@ import template from './searchresults.template.html'; IncludeGenres: false, IncludeStudios: false, IncludeArtists: false, - MediaTypes: 'Video', - ExcludeItemTypes: 'Movie,Episode' + IncludeItemTypes: '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', { + }, context, '.episodeResults', { 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 + showParentTitle: true }); } - function searchType(instance, apiClient, query, context, section, cardOptions) { - query.Limit = enableScrollX() ? 24 : 16; - query.ParentId = instance.options.parentId; + 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 - getSearchHints(instance, apiClient, query).then(function (result) { - populateResults(result, context, section, cardOptions); - }); + }, 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'); } - function populateResults(result, context, section, cardOptions) { - section = context.querySelector(section); + const html = globalize.translateHtml(workingTemplate, 'core'); - const items = result.Items || result.SearchHints; + elem.innerHTML = html; - 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(''); - } + elem.classList.add('searchResults'); + instance.search(''); +} class SearchResults { constructor(options) { @@ -620,5 +618,3 @@ class SearchResults { } export default SearchResults; - -/* eslint-enable indent */ From dd19ef5df423d3536bea5ca2499073d2929da309 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 16:35:27 -0400 Subject: [PATCH 08/16] Unmount components when view is destroyed --- src/components/reactControllerFactory.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/reactControllerFactory.js b/src/components/reactControllerFactory.js index 5e2d46a95..9611b706e 100644 --- a/src/components/reactControllerFactory.js +++ b/src/components/reactControllerFactory.js @@ -1,13 +1,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; -// TODO: Probably need to rehydrate on view restores - 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); + }); } }; From 4bf1acbf4e61a15b2c2d00405df632c5a2546666 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 28 May 2021 17:31:13 -0400 Subject: [PATCH 09/16] Remove test page --- src/components/pages/TestPage.js | 7 ------- src/controllers/reactTest.html | 1 - src/scripts/routes.js | 6 ------ 3 files changed, 14 deletions(-) delete mode 100644 src/components/pages/TestPage.js delete mode 100644 src/controllers/reactTest.html diff --git a/src/components/pages/TestPage.js b/src/components/pages/TestPage.js deleted file mode 100644 index 2d4209161..000000000 --- a/src/components/pages/TestPage.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const TestPage = () => ( -

Hello from React!

-); - -export default TestPage; diff --git a/src/controllers/reactTest.html b/src/controllers/reactTest.html deleted file mode 100644 index a0cbc97a9..000000000 --- a/src/controllers/reactTest.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 102d7f58d..388c2a83e 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -24,12 +24,6 @@ import { appRouter } from '../components/appRouter'; appRouter.addRoute(path, newRoute); } - defineRoute({ - alias: '/reactTest.html', - path: 'reactTest.html', - pageComponent: 'TestPage' - }); - defineRoute({ alias: '/addserver.html', path: 'session/addServer/index.html', From bd4626c682403b08adc6dcbfad77b99955e70813 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 1 Jun 2021 15:15:51 -0400 Subject: [PATCH 10/16] Replace old search fields component with react component --- package-lock.json | 5 + package.json | 1 + .../alphaPicker/AlphaPickerComponent.js | 40 +++++++ .../search/SearchFieldsComponent.js | 83 ++++++++++--- src/components/search/searchfields.js | 111 ------------------ .../search/searchfields.template.html | 7 -- 6 files changed, 112 insertions(+), 135 deletions(-) create mode 100644 src/components/alphaPicker/AlphaPickerComponent.js delete mode 100644 src/components/search/searchfields.js delete mode 100644 src/components/search/searchfields.template.html diff --git a/package-lock.json b/package-lock.json index 70e88ee5e..c280d080d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6912,6 +6912,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", diff --git a/package.json b/package.json index 6cac8d4d5..218e2b090 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "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", diff --git a/src/components/alphaPicker/AlphaPickerComponent.js b/src/components/alphaPicker/AlphaPickerComponent.js new file mode 100644 index 000000000..717b9f969 --- /dev/null +++ b/src/components/alphaPicker/AlphaPickerComponent.js @@ -0,0 +1,40 @@ +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(); + }; + }, []); + + useEffect(() => { + + }, [ alphaPicker ]); + + return ( +
+ ); +}; + +AlphaPickerComponent.propTypes = { + onAlphaPicked: PropTypes.func +}; + +export default AlphaPickerComponent; diff --git a/src/components/search/SearchFieldsComponent.js b/src/components/search/SearchFieldsComponent.js index 10526fa16..652e2b718 100644 --- a/src/components/search/SearchFieldsComponent.js +++ b/src/components/search/SearchFieldsComponent.js @@ -1,36 +1,85 @@ -import { Events } from 'jellyfin-apiclient'; +import debounce from 'lodash-es/debounce'; import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; -import SearchFields from './searchfields'; +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 SearchFieldsComponent = ({ onSearch = () => {} }) => { - const [ searchFields, setSearchFields ] = useState(null); - const searchFieldsElement = useRef(null); + const element = useRef(null); + + const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch'); + + const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), []); useEffect(() => { - setSearchFields( - new SearchFields({ element: searchFieldsElement.current }) - ); + getSearchInput()?.addEventListener('input', e => { + debouncedOnSearch(normalizeInput(e.target?.value)); + }); + getSearchInput()?.focus(); return () => { - searchFields?.destroy(); + debouncedOnSearch.cancel(); }; }, []); - useEffect(() => { - if (searchFields) { - Events.on(searchFields, 'search', (e, value) => { - onSearch(value); - }); + 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; } - }, [ searchFields ]); + + searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); + }; return (
+ ref={element} + > +
+ +
+
+ {layoutManager.tv && !browser.tv && + + } +
); }; diff --git a/src/components/search/searchfields.js b/src/components/search/searchfields.js deleted file mode 100644 index 1844ad811..000000000 --- a/src/components/search/searchfields.js +++ /dev/null @@ -1,111 +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'; - -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; 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 @@ -
- -
- -
-
-
From 1c0c25d655a35896f164d681fd51544a9d5172f2 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 1 Jun 2021 15:19:02 -0400 Subject: [PATCH 11/16] Rename search fields component --- src/components/pages/SearchPage.js | 10 ++++------ .../{SearchFieldsComponent.js => SearchFields.js} | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) rename src/components/search/{SearchFieldsComponent.js => SearchFields.js} (95%) diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js index 8fa2c69dd..7730b96db 100644 --- a/src/components/pages/SearchPage.js +++ b/src/components/pages/SearchPage.js @@ -1,18 +1,16 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; -import SearchFieldsComponent from '../search/SearchFieldsComponent'; -import SearchResultsComponent from '../search/SearchResultsComponent'; +import SearchFields from '../search/SearchFields'; +import SearchResults from '../search/SearchResultsComponent'; const SearchPage = ({ serverId, parentId, collectionType }) => { const [ query, setQuery ] = useState(null); return ( <> - - + ({ const normalizeInput = (value = '') => value.trim(); -const SearchFieldsComponent = ({ onSearch = () => {} }) => { +const SearchFields = ({ onSearch = () => {} }) => { const element = useRef(null); const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch'); @@ -83,8 +83,8 @@ const SearchFieldsComponent = ({ onSearch = () => {} }) => { ); }; -SearchFieldsComponent.propTypes = { +SearchFields.propTypes = { onSearch: PropTypes.func }; -export default SearchFieldsComponent; +export default SearchFields; From 6058a512c4e2209fb3a246f90156c2f679f983d0 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 2 Jun 2021 10:00:24 -0400 Subject: [PATCH 12/16] Move search suggestions to react component --- src/components/pages/SearchPage.js | 7 +++ src/components/search/SearchSuggestions.js | 69 ++++++++++++++++++++++ src/components/search/searchresults.js | 39 ------------ 3 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 src/components/search/SearchSuggestions.js diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js index 7730b96db..ea37bfec5 100644 --- a/src/components/pages/SearchPage.js +++ b/src/components/pages/SearchPage.js @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import SearchFields from '../search/SearchFields'; import SearchResults from '../search/SearchResultsComponent'; +import SearchSuggestions from '../search/SearchSuggestions'; const SearchPage = ({ serverId, parentId, collectionType }) => { const [ query, setQuery ] = useState(null); @@ -10,6 +11,12 @@ const SearchPage = ({ serverId, parentId, collectionType }) => { return ( <> + {!query && + + } ({ + __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/searchresults.js b/src/components/search/searchresults.js index f34a8292b..263a2b94c 100644 --- a/src/components/search/searchresults.js +++ b/src/components/search/searchresults.js @@ -1,49 +1,12 @@ 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'; -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({ @@ -149,10 +112,8 @@ function getSearchHints(instance, apiClient, 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') { From de54dc636afb54638e9dfc2e56621c247efafccf Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 3 Jun 2021 10:11:49 -0400 Subject: [PATCH 13/16] Move search results to react components --- package-lock.json | 5 + package.json | 1 + src/components/pages/SearchPage.js | 9 +- src/components/search/LiveTVSearchResults.js | 195 ++++++ src/components/search/SearchResults.js | 258 ++++++++ .../search/SearchResultsComponent.js | 44 -- src/components/search/SearchResultsRow.js | 48 ++ src/components/search/searchresults.js | 581 ------------------ .../search/searchresults.template.html | 145 ----- 9 files changed, 515 insertions(+), 771 deletions(-) create mode 100644 src/components/search/LiveTVSearchResults.js create mode 100644 src/components/search/SearchResults.js delete mode 100644 src/components/search/SearchResultsComponent.js create mode 100644 src/components/search/SearchResultsRow.js delete mode 100644 src/components/search/searchresults.js delete mode 100644 src/components/search/searchresults.template.html diff --git a/package-lock.json b/package-lock.json index c280d080d..652a9cc9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3010,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", diff --git a/package.json b/package.json index 218e2b090..6cc0f8de3 100644 --- a/package.json +++ b/package.json @@ -58,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", diff --git a/src/components/pages/SearchPage.js b/src/components/pages/SearchPage.js index ea37bfec5..93e277988 100644 --- a/src/components/pages/SearchPage.js +++ b/src/components/pages/SearchPage.js @@ -2,8 +2,9 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; import SearchFields from '../search/SearchFields'; -import SearchResults from '../search/SearchResultsComponent'; +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); @@ -23,6 +24,12 @@ const SearchPage = ({ serverId, parentId, collectionType }) => { collectionType={collectionType} query={query} /> + ); }; diff --git a/src/components/search/LiveTVSearchResults.js b/src/components/search/LiveTVSearchResults.js new file mode 100644 index 000000000..c4ffa6a51 --- /dev/null +++ b/src/components/search/LiveTVSearchResults.js @@ -0,0 +1,195 @@ +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 +}; + +const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { + const [ apiClient, setApiClient ] = useState(); + 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 [ videos, setVideos ] = 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(() => { + if (serverId) setApiClient(ServerConnections.getApiClient(serverId)); + }, [ serverId ]); + + useEffect(() => { + // Reset state + setMovies([]); + setEpisodes([]); + setSports([]); + setKids([]); + setNews([]); + setPrograms([]); + setVideos([]); + + if (query && isLiveTV()) { + // 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)); + // NOTE: I believe this is supposed to be home videos, but it + // includes TV channels so it should probably be included for Live TV + // Videos row + fetchItems(apiClient, { + MediaTypes: 'Video', + ExcludeItemTypes: 'Movie,Episode' + }).then(result => setVideos(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/SearchResults.js b/src/components/search/SearchResults.js new file mode 100644 index 000000000..daf32d477 --- /dev/null +++ b/src/components/search/SearchResults.js @@ -0,0 +1,258 @@ +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 SearchResultsComponent = ({ serverId, parentId, collectionType, query }) => { + const [ apiClient, setApiClient ] = useState(); + const [ movies, setMovies ] = useState([]); + const [ shows, setShows ] = useState([]); + const [ episodes, setEpisodes ] = useState([]); + const [ programs, setPrograms ] = useState([]); + const [ videos, setVideos ] = 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(() => { + if (serverId) setApiClient(ServerConnections.getApiClient(serverId)); + }, [ serverId ]); + + useEffect(() => { + // Reset state + setMovies([]); + setShows([]); + setEpisodes([]); + setPrograms([]); + setVideos([]); + setPlaylists([]); + setArtists([]); + setAlbums([]); + setSongs([]); + setPhotoAlbums([]); + setPhotos([]); + setAudioBooks([]); + setBooks([]); + setPeople([]); + + if (query) { + // 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) { + // Programs row + fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' }) + .then(result => setPrograms(result.Items)); + // Videos row + fetchItems(apiClient, { + MediaTypes: 'Video', + ExcludeItemTypes: 'Movie,Episode' + }).then(result => setVideos(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 ( +
+ + + + + + + + + + + + + + +
+ ); +}; + +SearchResultsComponent.propTypes = { + serverId: PropTypes.string, + parentId: PropTypes.string, + collectionType: PropTypes.string, + query: PropTypes.string +}; + +export default SearchResultsComponent; diff --git a/src/components/search/SearchResultsComponent.js b/src/components/search/SearchResultsComponent.js deleted file mode 100644 index 925d74987..000000000 --- a/src/components/search/SearchResultsComponent.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; - -import SearchResults from './searchresults'; - -const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) => { - const [ searchResults, setSearchResults ] = useState(null); - const searchResultsElement = useRef(null); - - useEffect(() => { - setSearchResults( - new SearchResults({ - element: searchResultsElement.current, - serverId: serverId || ApiClient.serverId(), - parentId, - collectionType - }) - ); - - return () => { - searchResults?.destroy(); - }; - }, []); - - useEffect(() => { - searchResults?.search(query); - }, [ query ]); - - return ( -
- ); -}; - -SearchResultsComponent.propTypes = { - serverId: PropTypes.string, - parentId: PropTypes.string, - collectionType: PropTypes.string, - query: PropTypes.string -}; - -export default SearchResultsComponent; diff --git a/src/components/search/SearchResultsRow.js b/src/components/search/SearchResultsRow.js new file mode 100644 index 000000000..df40154cb --- /dev/null +++ b/src/components/search/SearchResultsRow.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; + +import cardBuilder from '../cardbuilder/cardBuilder'; + +// 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/searchresults.js b/src/components/search/searchresults.js deleted file mode 100644 index 263a2b94c..000000000 --- a/src/components/search/searchresults.js +++ /dev/null @@ -1,581 +0,0 @@ -import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; -import cardBuilder from '../cardbuilder/cardBuilder'; -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'; - -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'; - } else { - instance.mode = 'suggestions'; - } - - 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; 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}

- -
-
-
-
From 35f6944310114c360b04769037c02467b63a64e7 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 7 Jun 2021 11:55:01 -0400 Subject: [PATCH 14/16] Fix code smells and dependency issue --- .../alphaPicker/AlphaPickerComponent.js | 4 ---- src/components/search/LiveTVSearchResults.js | 10 +++++----- src/components/search/SearchResults.js | 16 ++++++++-------- src/components/search/SearchResultsRow.js | 3 +++ src/components/search/SearchSuggestions.js | 2 ++ 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/components/alphaPicker/AlphaPickerComponent.js b/src/components/alphaPicker/AlphaPickerComponent.js index 717b9f969..ae325c24f 100644 --- a/src/components/alphaPicker/AlphaPickerComponent.js +++ b/src/components/alphaPicker/AlphaPickerComponent.js @@ -21,10 +21,6 @@ const AlphaPickerComponent = ({ onAlphaPicked = () => {} }) => { }; }, []); - useEffect(() => { - - }, [ alphaPicker ]); - return (
{ - const [ apiClient, setApiClient ] = useState(); const [ movies, setMovies ] = useState([]); const [ episodes, setEpisodes ] = useState([]); const [ sports, setSports ] = useState([]); @@ -55,10 +57,6 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { const isLiveTV = () => collectionType === 'livetv'; - useEffect(() => { - if (serverId) setApiClient(ServerConnections.getApiClient(serverId)); - }, [ serverId ]); - useEffect(() => { // Reset state setMovies([]); @@ -70,6 +68,8 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { setVideos([]); if (query && isLiveTV()) { + const apiClient = ServerConnections.getApiClient(serverId); + // Movies row fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram', diff --git a/src/components/search/SearchResults.js b/src/components/search/SearchResults.js index daf32d477..7aaa55b50 100644 --- a/src/components/search/SearchResults.js +++ b/src/components/search/SearchResults.js @@ -6,8 +6,10 @@ import globalize from '../../scripts/globalize'; import ServerConnections from '../ServerConnections'; import SearchResultsRow from './SearchResultsRow'; -const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) => { - const [ apiClient, setApiClient ] = useState(); +/* + * 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([]); @@ -71,10 +73,6 @@ const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) = const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv'; - useEffect(() => { - if (serverId) setApiClient(ServerConnections.getApiClient(serverId)); - }, [ serverId ]); - useEffect(() => { // Reset state setMovies([]); @@ -93,6 +91,8 @@ const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) = setPeople([]); if (query) { + const apiClient = ServerConnections.getApiClient(serverId); + // Movie libraries if (!collectionType || isMovies()) { // Movies row @@ -248,11 +248,11 @@ const SearchResultsComponent = ({ serverId, parentId, collectionType, query }) = ); }; -SearchResultsComponent.propTypes = { +SearchResults.propTypes = { serverId: PropTypes.string, parentId: PropTypes.string, collectionType: PropTypes.string, query: PropTypes.string }; -export default SearchResultsComponent; +export default SearchResults; diff --git a/src/components/search/SearchResultsRow.js b/src/components/search/SearchResultsRow.js index df40154cb..3667d8486 100644 --- a/src/components/search/SearchResultsRow.js +++ b/src/components/search/SearchResultsRow.js @@ -3,6 +3,9 @@ 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 =/ diff --git a/src/components/search/SearchSuggestions.js b/src/components/search/SearchSuggestions.js index 23fab5cf1..fbfd2de8a 100644 --- a/src/components/search/SearchSuggestions.js +++ b/src/components/search/SearchSuggestions.js @@ -5,6 +5,8 @@ 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 =/ From 219d83ede35b398c41ca6a217fc4df69795efa42 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 9 Jun 2021 09:56:56 -0400 Subject: [PATCH 15/16] Fix video row header translation key --- src/components/search/LiveTVSearchResults.js | 2 +- src/components/search/SearchResults.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/search/LiveTVSearchResults.js b/src/components/search/LiveTVSearchResults.js index bae0b2961..aabceebe9 100644 --- a/src/components/search/LiveTVSearchResults.js +++ b/src/components/search/LiveTVSearchResults.js @@ -177,7 +177,7 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { cardOptions={CARD_OPTIONS} /> diff --git a/src/components/search/SearchResults.js b/src/components/search/SearchResults.js index 7aaa55b50..5d4eda07b 100644 --- a/src/components/search/SearchResults.js +++ b/src/components/search/SearchResults.js @@ -200,7 +200,7 @@ const SearchResults = ({ serverId, parentId, collectionType, query }) => { }} /> From bf8f004eb3e500e41de9ac213b65184a0448df37 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 9 Jun 2021 10:15:48 -0400 Subject: [PATCH 16/16] Add tv channels search row --- src/components/search/LiveTVSearchResults.js | 20 ++++++-------- src/components/search/SearchResults.js | 28 +++++++++++++------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/components/search/LiveTVSearchResults.js b/src/components/search/LiveTVSearchResults.js index aabceebe9..871c86a96 100644 --- a/src/components/search/LiveTVSearchResults.js +++ b/src/components/search/LiveTVSearchResults.js @@ -28,7 +28,7 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { const [ kids, setKids ] = useState([]); const [ news, setNews ] = useState([]); const [ programs, setPrograms ] = useState([]); - const [ videos, setVideos ] = useState([]); + const [ channels, setChannels ] = useState([]); const getDefaultParameters = () => ({ ParentId: parentId, @@ -65,7 +65,7 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { setKids([]); setNews([]); setPrograms([]); - setVideos([]); + setChannels([]); if (query && isLiveTV()) { const apiClient = ServerConnections.getApiClient(serverId); @@ -124,13 +124,9 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { IsKids: false, IsNews: false }).then(result => setPrograms(result.Items)); - // NOTE: I believe this is supposed to be home videos, but it - // includes TV channels so it should probably be included for Live TV - // Videos row - fetchItems(apiClient, { - MediaTypes: 'Video', - ExcludeItemTypes: 'Movie,Episode' - }).then(result => setVideos(result.Items)); + // Channels row + fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) + .then(result => setChannels(result.Items)); } }, [ query ]); @@ -177,9 +173,9 @@ const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => { cardOptions={CARD_OPTIONS} />
); diff --git a/src/components/search/SearchResults.js b/src/components/search/SearchResults.js index 5d4eda07b..3b6322e20 100644 --- a/src/components/search/SearchResults.js +++ b/src/components/search/SearchResults.js @@ -13,8 +13,9 @@ const SearchResults = ({ serverId, parentId, collectionType, query }) => { const [ movies, setMovies ] = useState([]); const [ shows, setShows ] = useState([]); const [ episodes, setEpisodes ] = useState([]); - const [ programs, setPrograms ] = 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([]); @@ -78,8 +79,9 @@ const SearchResults = ({ serverId, parentId, collectionType, query }) => { setMovies([]); setShows([]); setEpisodes([]); - setPrograms([]); setVideos([]); + setPrograms([]); + setChannels([]); setPlaylists([]); setArtists([]); setAlbums([]); @@ -133,14 +135,17 @@ const SearchResults = ({ serverId, parentId, collectionType, query }) => { // Other libraries do not support in-library search currently if (!collectionType) { - // Programs row - fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' }) - .then(result => setPrograms(result.Items)); // Videos row fetchItems(apiClient, { MediaTypes: 'Video', - ExcludeItemTypes: 'Movie,Episode' + 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)); @@ -184,6 +189,11 @@ const SearchResults = ({ serverId, parentId, collectionType, query }) => { showParentTitle: true }} /> + { }} />