mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
commit
ab23184da3
21 changed files with 1159 additions and 939 deletions
11
.eslintrc.js
11
.eslintrc.js
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
root: true,
|
||||
plugins: [
|
||||
'@babel',
|
||||
'react',
|
||||
'promise',
|
||||
'import',
|
||||
'eslint-comments'
|
||||
|
@ -18,11 +19,13 @@ module.exports = {
|
|||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true
|
||||
impliedStrict: true,
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
// 'plugin:promise/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:eslint-comments/recommended',
|
||||
|
@ -36,6 +39,7 @@ module.exports = {
|
|||
'comma-spacing': ['error'],
|
||||
'eol-last': ['error'],
|
||||
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'keyword-spacing': ['error'],
|
||||
'max-statements-per-line': ['error'],
|
||||
'no-floating-decimal': ['error'],
|
||||
|
@ -54,6 +58,11 @@ module.exports = {
|
|||
'space-infix-ops': 'error',
|
||||
'yoda': 'error'
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
|
|
|
@ -11,7 +11,8 @@ module.exports = {
|
|||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
],
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
|
|
369
package-lock.json
generated
369
package-lock.json
generated
|
@ -758,6 +758,15 @@
|
|||
"@babel/helper-plugin-utils": "^7.8.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-jsx": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz",
|
||||
"integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-logical-assignment-operators": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
|
||||
|
@ -1260,6 +1269,65 @@
|
|||
"@babel/helper-plugin-utils": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-display-name": {
|
||||
"version": "7.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.2.tgz",
|
||||
"integrity": "sha512-zCubvP+jjahpnFJvPaHPiGVfuVUjXHhFvJKQdNnsmSsiU9kR/rCZ41jHc++tERD2zV+p7Hr6is+t5b6iWTCqSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.13.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-jsx": {
|
||||
"version": "7.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.3.tgz",
|
||||
"integrity": "sha512-uuxuoUNVhdgYzERiHHFkE4dWoJx+UFVyuAl0aqN8P2/AKFHwqgUC5w2+4/PjpKXJsFgBlYAFXlUmDQ3k3DUkXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.12.13",
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-plugin-utils": "^7.13.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.12.13",
|
||||
"@babel/types": "^7.14.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
|
||||
"integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-jsx-development": {
|
||||
"version": "7.12.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz",
|
||||
"integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/plugin-transform-react-jsx": "^7.12.17"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-pure-annotations": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz",
|
||||
"integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.10.4",
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-regenerator": {
|
||||
"version": "7.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz",
|
||||
|
@ -1473,6 +1541,20 @@
|
|||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"@babel/preset-react": {
|
||||
"version": "7.13.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.13.13.tgz",
|
||||
"integrity": "sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.13.0",
|
||||
"@babel/helper-validator-option": "^7.12.17",
|
||||
"@babel/plugin-transform-react-display-name": "^7.12.13",
|
||||
"@babel/plugin-transform-react-jsx": "^7.13.12",
|
||||
"@babel/plugin-transform-react-jsx-development": "^7.12.17",
|
||||
"@babel/plugin-transform-react-pure-annotations": "^7.12.1"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
|
||||
|
@ -2362,6 +2444,18 @@
|
|||
"es-abstract": "^1.18.0-next.1"
|
||||
}
|
||||
},
|
||||
"array.prototype.flatmap": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz",
|
||||
"integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"arrify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
||||
|
@ -2916,6 +3010,11 @@
|
|||
"version": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"integrity": "sha512-5rjszPzcjFVoDEOarszcbax2WIGT3+fO+W212ZWg9+ylGJgxG1IIcCFjnnBbSdM0lNeIfmMGhhEGovIlr+1yBg=="
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
|
@ -4609,6 +4708,47 @@
|
|||
"integrity": "sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz",
|
||||
"integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.3",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"doctrine": "^2.1.0",
|
||||
"has": "^1.0.3",
|
||||
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"object.entries": "^1.1.3",
|
||||
"object.fromentries": "^2.0.4",
|
||||
"object.values": "^1.1.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"resolve": "^2.0.0-next.3",
|
||||
"string.prototype.matchall": "^4.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "2.0.0-next.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
|
||||
"integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-core-module": "^2.2.0",
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-rule-composer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
|
||||
|
@ -6041,6 +6181,17 @@
|
|||
"ipaddr.js": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"internal-slot": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||
"integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.1.0",
|
||||
"has": "^1.0.3",
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"interpret": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
|
||||
|
@ -6518,8 +6669,7 @@
|
|||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.1",
|
||||
|
@ -6594,6 +6744,16 @@
|
|||
"jquery": ">=1.9.1"
|
||||
}
|
||||
},
|
||||
"jsx-ast-utils": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
|
||||
"integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.2",
|
||||
"object.assign": "^4.1.2"
|
||||
}
|
||||
},
|
||||
"jszip": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz",
|
||||
|
@ -6757,6 +6917,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"lodash._reinterpolate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
|
||||
|
@ -6897,6 +7062,14 @@
|
|||
"integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==",
|
||||
"dev": true
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"loud-rejection": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||
|
@ -7510,8 +7683,7 @@
|
|||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"dev": true
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"object-copy": {
|
||||
"version": "0.1.0",
|
||||
|
@ -7593,6 +7765,77 @@
|
|||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"object.entries": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz",
|
||||
"integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.3",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz",
|
||||
"integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.2",
|
||||
"is-callable": "^1.2.3",
|
||||
"is-negative-zero": "^2.0.1",
|
||||
"is-regex": "^1.1.3",
|
||||
"is-string": "^1.0.6",
|
||||
"object-inspect": "^1.10.3",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.2",
|
||||
"string.prototype.trimend": "^1.0.4",
|
||||
"string.prototype.trimstart": "^1.0.4",
|
||||
"unbox-primitive": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
|
||||
"integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"is-string": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
|
||||
"integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
|
||||
"dev": true
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.fromentries": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz",
|
||||
"integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.0-next.2",
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"object.getownpropertydescriptors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz",
|
||||
|
@ -9267,6 +9510,16 @@
|
|||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
}
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||
|
@ -9379,6 +9632,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"scheduler": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"read-file-stdin": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
|
||||
|
@ -9957,6 +10234,15 @@
|
|||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"dev": true
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
|
||||
|
@ -10181,6 +10467,17 @@
|
|||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
|
@ -10636,6 +10933,70 @@
|
|||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"string.prototype.matchall": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz",
|
||||
"integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.18.2",
|
||||
"get-intrinsic": "^1.1.1",
|
||||
"has-symbols": "^1.0.2",
|
||||
"internal-slot": "^1.0.3",
|
||||
"regexp.prototype.flags": "^1.3.1",
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.3",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz",
|
||||
"integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.2",
|
||||
"is-callable": "^1.2.3",
|
||||
"is-negative-zero": "^2.0.1",
|
||||
"is-regex": "^1.1.3",
|
||||
"is-string": "^1.0.6",
|
||||
"object-inspect": "^1.10.3",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.2",
|
||||
"string.prototype.trimend": "^1.0.4",
|
||||
"string.prototype.trimstart": "^1.0.4",
|
||||
"unbox-primitive": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
|
||||
"integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"is-string": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
|
||||
"integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
|
||||
"dev": true
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"string.prototype.trimend": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@babel/plugin-proposal-private-methods": "^7.12.13",
|
||||
"@babel/plugin-transform-modules-umd": "^7.14.0",
|
||||
"@babel/preset-env": "^7.14.1",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@uupaa/dynamic-import-polyfill": "^1.0.2",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"expose-loader": "^2.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^1.1.0",
|
||||
|
@ -56,6 +58,7 @@
|
|||
"@fontsource/noto-sans-sc": "^4.2.1",
|
||||
"blurhash": "^1.1.3",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "^3.11.2",
|
||||
"date-fns": "^2.21.1",
|
||||
"epubjs": "^0.3.85",
|
||||
|
@ -69,10 +72,14 @@
|
|||
"jstree": "^3.3.11",
|
||||
"libarchive.js": "^1.3.0",
|
||||
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
|
||||
"lodash-es": "^4.17.21",
|
||||
"material-design-icons-iconfont": "^6.1.0",
|
||||
"native-promise-only": "^0.8.0-a",
|
||||
"page": "^1.11.6",
|
||||
"pdfjs-dist": "2.6.347",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.1.0",
|
||||
"sortablejs": "^1.13.0",
|
||||
|
|
36
src/components/alphaPicker/AlphaPickerComponent.js
Normal file
36
src/components/alphaPicker/AlphaPickerComponent.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import AlphaPicker from './alphaPicker';
|
||||
|
||||
// React compatibility wrapper component for alphaPicker.js
|
||||
const AlphaPickerComponent = ({ onAlphaPicked = () => {} }) => {
|
||||
const [ alphaPicker, setAlphaPicker ] = useState(null);
|
||||
const element = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setAlphaPicker(new AlphaPicker({
|
||||
element: element.current,
|
||||
mode: 'keyboard'
|
||||
}));
|
||||
|
||||
element.current?.addEventListener('alphavalueclicked', onAlphaPicked);
|
||||
|
||||
return () => {
|
||||
alphaPicker?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={element}
|
||||
className='alphaPicker align-items-center'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AlphaPickerComponent.propTypes = {
|
||||
onAlphaPicked: PropTypes.func
|
||||
};
|
||||
|
||||
export default AlphaPickerComponent;
|
|
@ -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
|
||||
},
|
||||
|
|
43
src/components/pages/SearchPage.js
Normal file
43
src/components/pages/SearchPage.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import SearchFields from '../search/SearchFields';
|
||||
import SearchResults from '../search/SearchResults';
|
||||
import SearchSuggestions from '../search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../search/LiveTVSearchResults';
|
||||
|
||||
const SearchPage = ({ serverId, parentId, collectionType }) => {
|
||||
const [ query, setQuery ] = useState(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchFields onSearch={setQuery} />
|
||||
{!query &&
|
||||
<SearchSuggestions
|
||||
serverId={serverId || ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
/>
|
||||
}
|
||||
<SearchResults
|
||||
serverId={serverId || ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
collectionType={collectionType}
|
||||
query={query}
|
||||
/>
|
||||
<LiveTVSearchResults
|
||||
serverId={serverId || ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
collectionType={collectionType}
|
||||
query={query}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SearchPage.propTypes = {
|
||||
serverId: PropTypes.string,
|
||||
parentId: PropTypes.string,
|
||||
collectionType: PropTypes.string
|
||||
};
|
||||
|
||||
export default SearchPage;
|
17
src/components/reactControllerFactory.js
Normal file
17
src/components/reactControllerFactory.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default (view, params, { detail }) => {
|
||||
if (detail.options?.pageComponent) {
|
||||
// Fetch and render the page component to the view
|
||||
import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`)
|
||||
.then(({ default: component }) => {
|
||||
ReactDOM.render(React.createElement(component, params), view);
|
||||
});
|
||||
|
||||
// Unmount component when view is destroyed
|
||||
view.addEventListener('viewdestroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(view);
|
||||
});
|
||||
}
|
||||
};
|
191
src/components/search/LiveTVSearchResults.js
Normal file
191
src/components/search/LiveTVSearchResults.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import SearchResultsRow from './SearchResultsRow';
|
||||
|
||||
const CARD_OPTIONS = {
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
coverImage: true,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
};
|
||||
|
||||
/*
|
||||
* React component to display search result rows for live tv library search
|
||||
*/
|
||||
const LiveTVSearchResults = ({ serverId, parentId, collectionType, query }) => {
|
||||
const [ movies, setMovies ] = useState([]);
|
||||
const [ episodes, setEpisodes ] = useState([]);
|
||||
const [ sports, setSports ] = useState([]);
|
||||
const [ kids, setKids ] = useState([]);
|
||||
const [ news, setNews ] = useState([]);
|
||||
const [ programs, setPrograms ] = useState([]);
|
||||
const [ channels, setChannels ] = useState([]);
|
||||
|
||||
const getDefaultParameters = () => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
ImageTypeLimit: 1,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false
|
||||
});
|
||||
|
||||
// FIXME: This query does not support Live TV filters
|
||||
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
|
||||
apiClient?.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeMedia: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
|
||||
const isLiveTV = () => collectionType === 'livetv';
|
||||
|
||||
useEffect(() => {
|
||||
// Reset state
|
||||
setMovies([]);
|
||||
setEpisodes([]);
|
||||
setSports([]);
|
||||
setKids([]);
|
||||
setNews([]);
|
||||
setPrograms([]);
|
||||
setChannels([]);
|
||||
|
||||
if (query && isLiveTV()) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
// Movies row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: true,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setMovies(result.Items));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: true,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setEpisodes(result.Items));
|
||||
// Sports row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: true,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setSports(result.Items));
|
||||
// Kids row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: true,
|
||||
IsNews: false
|
||||
}).then(result => setKids(result.Items));
|
||||
// News row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: true
|
||||
}).then(result => setNews(result.Items));
|
||||
// Programs row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setPrograms(result.Items));
|
||||
// Channels row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
||||
.then(result => setChannels(result.Items));
|
||||
}
|
||||
}, [ query ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'searchResults',
|
||||
'padded-bottom-page',
|
||||
'padded-top',
|
||||
{ 'hide': !query || !isLiveTV() }
|
||||
)}
|
||||
>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Movies')}
|
||||
items={movies}
|
||||
cardOptions={{
|
||||
...CARD_OPTIONS,
|
||||
shape: 'overflowPortrait'
|
||||
}}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Episodes')}
|
||||
items={episodes}
|
||||
cardOptions={CARD_OPTIONS}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Sports')}
|
||||
items={sports}
|
||||
cardOptions={CARD_OPTIONS}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Kids')}
|
||||
items={kids}
|
||||
cardOptions={CARD_OPTIONS}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('News')}
|
||||
items={news}
|
||||
cardOptions={CARD_OPTIONS}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Programs')}
|
||||
items={programs}
|
||||
cardOptions={CARD_OPTIONS}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Channels')}
|
||||
items={channels}
|
||||
cardOptions={{ shape: 'square' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LiveTVSearchResults.propTypes = {
|
||||
serverId: PropTypes.string,
|
||||
parentId: PropTypes.string,
|
||||
collectionType: PropTypes.string,
|
||||
query: PropTypes.string
|
||||
};
|
||||
|
||||
export default LiveTVSearchResults;
|
90
src/components/search/SearchFields.js
Normal file
90
src/components/search/SearchFields.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import debounce from 'lodash-es/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
||||
import 'material-design-icons-iconfont';
|
||||
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../assets/css/flexstyles.scss';
|
||||
import './searchfields.scss';
|
||||
import layoutManager from '../layoutManager';
|
||||
import browser from '../../scripts/browser';
|
||||
|
||||
// There seems to be some compatibility issues here between
|
||||
// React and our legacy web components, so we need to inject
|
||||
// them as an html string for now =/
|
||||
const createInputElement = () => ({
|
||||
__html: `<input
|
||||
is="emby-input"
|
||||
class="searchfields-txtSearch"
|
||||
type="text"
|
||||
data-keyboard="true"
|
||||
placeholder="${globalize.translate('Search')}"
|
||||
autocomplete="off"
|
||||
maxlength="40"
|
||||
autofocus
|
||||
/>`
|
||||
});
|
||||
|
||||
const normalizeInput = (value = '') => value.trim();
|
||||
|
||||
const SearchFields = ({ onSearch = () => {} }) => {
|
||||
const element = useRef(null);
|
||||
|
||||
const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch');
|
||||
|
||||
const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), []);
|
||||
|
||||
useEffect(() => {
|
||||
getSearchInput()?.addEventListener('input', e => {
|
||||
debouncedOnSearch(normalizeInput(e.target?.value));
|
||||
});
|
||||
getSearchInput()?.focus();
|
||||
|
||||
return () => {
|
||||
debouncedOnSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onAlphaPicked = e => {
|
||||
const value = e.detail.value;
|
||||
const searchInput = getSearchInput();
|
||||
|
||||
if (value === 'backspace') {
|
||||
const currentValue = searchInput.value;
|
||||
searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : '';
|
||||
} else {
|
||||
searchInput.value += value;
|
||||
}
|
||||
|
||||
searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className='padded-left padded-right searchFields'
|
||||
ref={element}
|
||||
>
|
||||
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
||||
<span className='searchfields-icon material-icons search' />
|
||||
<div
|
||||
className='inputContainer flex-grow'
|
||||
style={{ marginBottom: 0 }}
|
||||
dangerouslySetInnerHTML={createInputElement()}
|
||||
/>
|
||||
</div>
|
||||
{layoutManager.tv && !browser.tv &&
|
||||
<AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchFields.propTypes = {
|
||||
onSearch: PropTypes.func
|
||||
};
|
||||
|
||||
export default SearchFields;
|
268
src/components/search/SearchResults.js
Normal file
268
src/components/search/SearchResults.js
Normal file
|
@ -0,0 +1,268 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import SearchResultsRow from './SearchResultsRow';
|
||||
|
||||
/*
|
||||
* React component to display search result rows for global search and non-live tv library search
|
||||
*/
|
||||
const SearchResults = ({ serverId, parentId, collectionType, query }) => {
|
||||
const [ movies, setMovies ] = useState([]);
|
||||
const [ shows, setShows ] = useState([]);
|
||||
const [ episodes, setEpisodes ] = useState([]);
|
||||
const [ videos, setVideos ] = useState([]);
|
||||
const [ programs, setPrograms ] = useState([]);
|
||||
const [ channels, setChannels ] = useState([]);
|
||||
const [ playlists, setPlaylists ] = useState([]);
|
||||
const [ artists, setArtists ] = useState([]);
|
||||
const [ albums, setAlbums ] = useState([]);
|
||||
const [ songs, setSongs ] = useState([]);
|
||||
const [ photoAlbums, setPhotoAlbums ] = useState([]);
|
||||
const [ photos, setPhotos ] = useState([]);
|
||||
const [ audioBooks, setAudioBooks ] = useState([]);
|
||||
const [ books, setBooks ] = useState([]);
|
||||
const [ people, setPeople ] = useState([]);
|
||||
|
||||
const getDefaultParameters = () => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
ImageTypeLimit: 1,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false
|
||||
});
|
||||
|
||||
const fetchArtists = (apiClient, params = {}) => apiClient?.getArtists(
|
||||
apiClient?.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeArtists: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
|
||||
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
|
||||
apiClient?.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeMedia: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
|
||||
const fetchPeople = (apiClient, params = {}) => apiClient?.getPeople(
|
||||
apiClient?.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludePeople: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
|
||||
const isMovies = () => collectionType === 'movies';
|
||||
|
||||
const isMusic = () => collectionType === 'music';
|
||||
|
||||
const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv';
|
||||
|
||||
useEffect(() => {
|
||||
// Reset state
|
||||
setMovies([]);
|
||||
setShows([]);
|
||||
setEpisodes([]);
|
||||
setVideos([]);
|
||||
setPrograms([]);
|
||||
setChannels([]);
|
||||
setPlaylists([]);
|
||||
setArtists([]);
|
||||
setAlbums([]);
|
||||
setSongs([]);
|
||||
setPhotoAlbums([]);
|
||||
setPhotos([]);
|
||||
setAudioBooks([]);
|
||||
setBooks([]);
|
||||
setPeople([]);
|
||||
|
||||
if (query) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
// Movie libraries
|
||||
if (!collectionType || isMovies()) {
|
||||
// Movies row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
|
||||
.then(result => setMovies(result.Items));
|
||||
}
|
||||
|
||||
// TV Show libraries
|
||||
if (!collectionType || isTVShows()) {
|
||||
// Shows row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
|
||||
.then(result => setShows(result.Items));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
|
||||
.then(result => setEpisodes(result.Items));
|
||||
}
|
||||
|
||||
// People are included for Movies and TV Shows
|
||||
if (!collectionType || isMovies() || isTVShows()) {
|
||||
// People row
|
||||
fetchPeople(apiClient).then(result => setPeople(result.Items));
|
||||
}
|
||||
|
||||
// Music libraries
|
||||
if (!collectionType || isMusic()) {
|
||||
// Playlists row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
|
||||
.then(results => setPlaylists(results.Items));
|
||||
// Artists row
|
||||
fetchArtists(apiClient).then(result => setArtists(result.Items));
|
||||
// Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
|
||||
.then(result => setAlbums(result.Items));
|
||||
// Songs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
|
||||
.then(result => setSongs(result.Items));
|
||||
}
|
||||
|
||||
// Other libraries do not support in-library search currently
|
||||
if (!collectionType) {
|
||||
// Videos row
|
||||
fetchItems(apiClient, {
|
||||
MediaTypes: 'Video',
|
||||
ExcludeItemTypes: 'Movie,Episode,TvChannel'
|
||||
}).then(result => setVideos(result.Items));
|
||||
// Programs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
|
||||
.then(result => setPrograms(result.Items));
|
||||
// Channels row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
||||
.then(result => setChannels(result.Items));
|
||||
// Photo Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
|
||||
.then(results => setPhotoAlbums(results.Items));
|
||||
// Photos row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
|
||||
.then(results => setPhotos(results.Items));
|
||||
// Audio Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
|
||||
.then(results => setAudioBooks(results.Items));
|
||||
// Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
|
||||
.then(results => setBooks(results.Items));
|
||||
}
|
||||
}
|
||||
}, [ query ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'searchResults',
|
||||
'padded-bottom-page',
|
||||
'padded-top',
|
||||
{ 'hide': !query || collectionType === 'livetv' }
|
||||
)}
|
||||
>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Movies')}
|
||||
items={movies}
|
||||
cardOptions={{ showYear: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Shows')}
|
||||
items={shows}
|
||||
cardOptions={{ showYear: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Episodes')}
|
||||
items={episodes}
|
||||
cardOptions={{
|
||||
coverImage: true,
|
||||
showParentTitle: true
|
||||
}}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('HeaderVideos')}
|
||||
items={videos}
|
||||
cardOptions={{ showParentTitle: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Programs')}
|
||||
items={programs}
|
||||
cardOptions={{
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
coverImage: true,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
}}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Channels')}
|
||||
items={channels}
|
||||
cardOptions={{ shape: 'square' }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Playlists')}
|
||||
items={playlists}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Artists')}
|
||||
items={artists}
|
||||
cardOptions={{ coverImage: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Albums')}
|
||||
items={albums}
|
||||
cardOptions={{ showParentTitle: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Songs')}
|
||||
items={songs}
|
||||
cardOptions={{ showParentTitle: true }}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('HeaderPhotoAlbums')}
|
||||
items={photoAlbums}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Photos')}
|
||||
items={photos}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('HeaderAudioBooks')}
|
||||
items={audioBooks}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Books')}
|
||||
items={books}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('People')}
|
||||
items={people}
|
||||
cardOptions={{ coverImage: true }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchResults.propTypes = {
|
||||
serverId: PropTypes.string,
|
||||
parentId: PropTypes.string,
|
||||
collectionType: PropTypes.string,
|
||||
query: PropTypes.string
|
||||
};
|
||||
|
||||
export default SearchResults;
|
51
src/components/search/SearchResultsRow.js
Normal file
51
src/components/search/SearchResultsRow.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
||||
import '../../elements/emby-scroller/emby-scroller';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
// There seems to be some compatibility issues here between
|
||||
// React and our legacy web components, so we need to inject
|
||||
// them as an html string for now =/
|
||||
const createScroller = ({ title = '' }) => ({
|
||||
__html: `<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${title}</h2>
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>`
|
||||
});
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={element}
|
||||
className='verticalSection'
|
||||
dangerouslySetInnerHTML={createScroller({ title })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SearchResultsRow.propTypes = {
|
||||
title: PropTypes.string,
|
||||
items: PropTypes.array,
|
||||
cardOptions: PropTypes.object
|
||||
};
|
||||
|
||||
export default SearchResultsRow;
|
71
src/components/search/SearchSuggestions.js
Normal file
71
src/components/search/SearchSuggestions.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { appRouter } from '../appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
|
||||
// There seems to be some compatibility issues here between
|
||||
// React and our legacy web components, so we need to inject
|
||||
// them as an html string for now =/
|
||||
const createSuggestionLink = ({name, href}) => ({
|
||||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
style='display: inline-block; padding: 0.5em 1em;'
|
||||
href='${href}'
|
||||
>${name}</a>`
|
||||
});
|
||||
|
||||
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 (
|
||||
<div
|
||||
className='verticalSection searchSuggestions'
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
<div>
|
||||
<h2 className='sectionTitle padded-left padded-right'>
|
||||
{globalize.translate('Suggestions')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='searchSuggestionsList padded-left padded-right'>
|
||||
{suggestions.map(item => (
|
||||
<div
|
||||
key={`suggestion-${item.Id}`}
|
||||
dangerouslySetInnerHTML={createSuggestionLink({
|
||||
name: item.Name,
|
||||
href: appRouter.getRouteUrl(item)
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchSuggestions.propTypes = {
|
||||
parentId: PropTypes.string,
|
||||
serverId: PropTypes.string
|
||||
};
|
||||
|
||||
export default SearchSuggestions;
|
|
@ -1,115 +0,0 @@
|
|||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import browser from '../../scripts/browser';
|
||||
import AlphaPicker from '../alphaPicker/alphaPicker';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../assets/css/flexstyles.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import './searchfields.scss';
|
||||
import template from './searchfields.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function onSearchTimeout() {
|
||||
const instance = this;
|
||||
let value = instance.nextSearchValue;
|
||||
|
||||
value = (value || '').trim();
|
||||
Events.trigger(instance, 'search', [value]);
|
||||
}
|
||||
|
||||
function triggerSearch(instance, value) {
|
||||
if (instance.searchTimeout) {
|
||||
clearTimeout(instance.searchTimeout);
|
||||
}
|
||||
|
||||
instance.nextSearchValue = value;
|
||||
instance.searchTimeout = setTimeout(onSearchTimeout.bind(instance), 400);
|
||||
}
|
||||
|
||||
function onAlphaValueClicked(e) {
|
||||
const value = e.detail.value;
|
||||
const searchFieldsInstance = this;
|
||||
|
||||
const txtSearch = searchFieldsInstance.options.element.querySelector('.searchfields-txtSearch');
|
||||
|
||||
if (value === 'backspace') {
|
||||
const val = txtSearch.value;
|
||||
txtSearch.value = val.length ? val.substring(0, val.length - 1) : '';
|
||||
} else {
|
||||
txtSearch.value += value;
|
||||
}
|
||||
|
||||
txtSearch.dispatchEvent(new CustomEvent('input', {
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
|
||||
function initAlphaPicker(alphaPickerElement, instance) {
|
||||
instance.alphaPicker = new AlphaPicker({
|
||||
element: alphaPickerElement,
|
||||
mode: 'keyboard'
|
||||
});
|
||||
|
||||
alphaPickerElement.addEventListener('alphavalueclicked', onAlphaValueClicked.bind(instance));
|
||||
}
|
||||
|
||||
function onSearchInput(e) {
|
||||
const value = e.target.value;
|
||||
const searchFieldsInstance = this;
|
||||
triggerSearch(searchFieldsInstance, value);
|
||||
}
|
||||
|
||||
function embed(elem, instance) {
|
||||
elem.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
elem.classList.add('searchFields');
|
||||
|
||||
const txtSearch = elem.querySelector('.searchfields-txtSearch');
|
||||
|
||||
if (layoutManager.tv && !browser.tv) {
|
||||
const alphaPickerElement = elem.querySelector('.alphaPicker');
|
||||
|
||||
elem.querySelector('.alphaPicker').classList.remove('hide');
|
||||
initAlphaPicker(alphaPickerElement, instance);
|
||||
}
|
||||
|
||||
txtSearch.addEventListener('input', onSearchInput.bind(instance));
|
||||
|
||||
instance.focus();
|
||||
}
|
||||
|
||||
class SearchFields {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options.element, this);
|
||||
}
|
||||
focus() {
|
||||
this.options.element.querySelector('.searchfields-txtSearch').focus();
|
||||
}
|
||||
destroy() {
|
||||
const options = this.options;
|
||||
if (options) {
|
||||
options.element.classList.remove('searchFields');
|
||||
}
|
||||
this.options = null;
|
||||
|
||||
const alphaPicker = this.alphaPicker;
|
||||
if (alphaPicker) {
|
||||
alphaPicker.destroy();
|
||||
}
|
||||
this.alphaPicker = null;
|
||||
|
||||
const searchTimeout = this.searchTimeout;
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
this.searchTimeout = null;
|
||||
this.nextSearchValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchFields;
|
||||
|
||||
/* eslint-enable indent */
|
|
@ -1,7 +0,0 @@
|
|||
<div class="searchFieldsInner flex align-items-center justify-content-center">
|
||||
<span class="searchfields-icon material-icons search"></span>
|
||||
<div class="inputContainer flex-grow" style="margin-bottom: 0;">
|
||||
<input is="emby-input" class="searchfields-txtSearch" type="text" data-keyboard="true" placeholder="${Search}" autocomplete="off" maxlength="40" autofocus />
|
||||
</div>
|
||||
</div>
|
||||
<div class="alphaPicker align-items-center hide"></div>
|
|
@ -1,624 +0,0 @@
|
|||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import { appRouter } from '../appRouter';
|
||||
import '../../elements/emby-scroller/emby-scroller';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './searchresults.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function loadSuggestions(instance, context, apiClient) {
|
||||
const options = {
|
||||
|
||||
SortBy: 'IsFavoriteOrLiked,Random',
|
||||
IncludeItemTypes: 'Movie,Series,MusicArtist',
|
||||
Limit: 20,
|
||||
Recursive: true,
|
||||
ImageTypeLimit: 0,
|
||||
EnableImages: false,
|
||||
ParentId: instance.options.parentId,
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(function (result) {
|
||||
if (instance.mode !== 'suggestions') {
|
||||
result.Items = [];
|
||||
}
|
||||
|
||||
const html = result.Items.map(function (i) {
|
||||
const href = appRouter.getRouteUrl(i);
|
||||
|
||||
let itemHtml = '<div><a is="emby-linkbutton" class="button-link" style="display:inline-block;padding:.5em 1em;" href="' + href + '">';
|
||||
itemHtml += i.Name;
|
||||
itemHtml += '</a></div>';
|
||||
return itemHtml;
|
||||
}).join('');
|
||||
|
||||
const searchSuggestions = context.querySelector('.searchSuggestions');
|
||||
searchSuggestions.querySelector('.searchSuggestionsList').innerHTML = html;
|
||||
|
||||
if (result.Items.length) {
|
||||
searchSuggestions.classList.remove('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSearchHints(instance, apiClient, query) {
|
||||
if (!query.searchTerm) {
|
||||
return Promise.resolve({
|
||||
SearchHints: []
|
||||
});
|
||||
}
|
||||
|
||||
let allowSearch = true;
|
||||
|
||||
const queryIncludeItemTypes = query.IncludeItemTypes;
|
||||
|
||||
if (instance.options.collectionType === 'tvshows') {
|
||||
if (query.IncludeArtists) {
|
||||
allowSearch = false;
|
||||
} else if (queryIncludeItemTypes === 'Movie' ||
|
||||
queryIncludeItemTypes === 'LiveTvProgram' ||
|
||||
queryIncludeItemTypes === 'MusicAlbum' ||
|
||||
queryIncludeItemTypes === 'Audio' ||
|
||||
queryIncludeItemTypes === 'Book' ||
|
||||
queryIncludeItemTypes === 'AudioBook' ||
|
||||
queryIncludeItemTypes === 'Playlist' ||
|
||||
queryIncludeItemTypes === 'PhotoAlbum' ||
|
||||
query.MediaTypes === 'Video' ||
|
||||
query.MediaTypes === 'Photo') {
|
||||
allowSearch = false;
|
||||
}
|
||||
} else if (instance.options.collectionType === 'movies') {
|
||||
if (query.IncludeArtists) {
|
||||
allowSearch = false;
|
||||
} else if (queryIncludeItemTypes === 'Series' ||
|
||||
queryIncludeItemTypes === 'Episode' ||
|
||||
queryIncludeItemTypes === 'LiveTvProgram' ||
|
||||
queryIncludeItemTypes === 'MusicAlbum' ||
|
||||
queryIncludeItemTypes === 'Audio' ||
|
||||
queryIncludeItemTypes === 'Book' ||
|
||||
queryIncludeItemTypes === 'AudioBook' ||
|
||||
queryIncludeItemTypes === 'Playlist' ||
|
||||
queryIncludeItemTypes === 'PhotoAlbum' ||
|
||||
query.MediaTypes === 'Video' ||
|
||||
query.MediaTypes === 'Photo') {
|
||||
allowSearch = false;
|
||||
}
|
||||
} else if (instance.options.collectionType === 'music') {
|
||||
if (query.People) {
|
||||
allowSearch = false;
|
||||
} else if (queryIncludeItemTypes === 'Series' ||
|
||||
queryIncludeItemTypes === 'Episode' ||
|
||||
queryIncludeItemTypes === 'LiveTvProgram' ||
|
||||
queryIncludeItemTypes === 'Movie') {
|
||||
allowSearch = false;
|
||||
}
|
||||
} else if (instance.options.collectionType === 'livetv') {
|
||||
if (query.IncludeArtists || query.IncludePeople) {
|
||||
allowSearch = false;
|
||||
} else if (queryIncludeItemTypes === 'Series' ||
|
||||
queryIncludeItemTypes === 'Episode' ||
|
||||
queryIncludeItemTypes === 'MusicAlbum' ||
|
||||
queryIncludeItemTypes === 'Audio' ||
|
||||
queryIncludeItemTypes === 'Book' ||
|
||||
queryIncludeItemTypes === 'AudioBook' ||
|
||||
queryIncludeItemTypes === 'PhotoAlbum' ||
|
||||
queryIncludeItemTypes === 'Movie' ||
|
||||
query.MediaTypes === 'Video' ||
|
||||
query.MediaTypes === 'Photo') {
|
||||
allowSearch = false;
|
||||
}
|
||||
}
|
||||
if (queryIncludeItemTypes === 'NullType') {
|
||||
allowSearch = false;
|
||||
}
|
||||
|
||||
if (!allowSearch) {
|
||||
return Promise.resolve({
|
||||
SearchHints: []
|
||||
});
|
||||
}
|
||||
|
||||
// Convert the search hint query to a regular item query
|
||||
if (apiClient.isMinServerVersion('3.4.1.31')) {
|
||||
query.Fields = 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount';
|
||||
query.Recursive = true;
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.ImageTypeLimit = 1;
|
||||
|
||||
let methodName = 'getItems';
|
||||
|
||||
if (!query.IncludeMedia) {
|
||||
if (query.IncludePeople) {
|
||||
methodName = 'getPeople';
|
||||
} else if (query.IncludeArtists) {
|
||||
methodName = 'getArtists';
|
||||
}
|
||||
}
|
||||
|
||||
return apiClient[methodName](apiClient.getCurrentUserId(), query);
|
||||
}
|
||||
|
||||
query.UserId = apiClient.getCurrentUserId();
|
||||
|
||||
return apiClient.getSearchHints(query);
|
||||
}
|
||||
|
||||
function search(instance, apiClient, context, value) {
|
||||
if (value || layoutManager.tv) {
|
||||
instance.mode = 'search';
|
||||
context.querySelector('.searchSuggestions').classList.add('hide');
|
||||
} else {
|
||||
instance.mode = 'suggestions';
|
||||
loadSuggestions(instance, context, apiClient);
|
||||
}
|
||||
|
||||
if (instance.options.collectionType === 'livetv') {
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: true,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
|
||||
}, context, '.movieResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowPortrait' : 'portrait'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
});
|
||||
} else {
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Movie'
|
||||
|
||||
}, context, '.movieResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true,
|
||||
showYear: true
|
||||
});
|
||||
}
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Series'
|
||||
|
||||
}, context, '.seriesResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true,
|
||||
showYear: true
|
||||
});
|
||||
|
||||
if (instance.options.collectionType === 'livetv') {
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsSeries: true,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
|
||||
}, context, '.episodeResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
});
|
||||
} else {
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Episode'
|
||||
|
||||
}, context, '.episodeResults', {
|
||||
|
||||
coverImage: true,
|
||||
showTitle: true,
|
||||
showParentTitle: true
|
||||
});
|
||||
}
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
// NullType to hide
|
||||
IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType',
|
||||
IsSports: true
|
||||
|
||||
}, context, '.sportsResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
// NullType to hide
|
||||
IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType',
|
||||
IsKids: true
|
||||
|
||||
}, context, '.kidsResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
// NullType to hide
|
||||
IncludeItemTypes: instance.options.collectionType === 'livetv' ? 'LiveTvProgram' : 'NullType',
|
||||
IsNews: true
|
||||
|
||||
}, context, '.newsResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: instance.options.collectionType === 'livetv' ? false : null,
|
||||
IsSeries: instance.options.collectionType === 'livetv' ? false : null,
|
||||
IsSports: instance.options.collectionType === 'livetv' ? false : null,
|
||||
IsKids: instance.options.collectionType === 'livetv' ? false : null,
|
||||
IsNews: instance.options.collectionType === 'livetv' ? false : null
|
||||
|
||||
}, context, '.programResults', {
|
||||
|
||||
preferThumb: true,
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'overflowBackdrop' : 'backdrop'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: false,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
showAirTime: true,
|
||||
showAirDateTime: true,
|
||||
showChannelName: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
MediaTypes: 'Video',
|
||||
ExcludeItemTypes: 'Movie,Episode'
|
||||
|
||||
}, context, '.videoResults', {
|
||||
|
||||
showParentTitle: true,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: true,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false
|
||||
|
||||
}, context, '.peopleResults', {
|
||||
|
||||
coverImage: true,
|
||||
showTitle: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: true
|
||||
|
||||
}, context, '.artistResults', {
|
||||
coverImage: true,
|
||||
showTitle: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'MusicAlbum'
|
||||
|
||||
}, context, '.albumResults', {
|
||||
|
||||
showParentTitle: true,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Audio'
|
||||
|
||||
}, context, '.songResults', {
|
||||
|
||||
showParentTitle: true,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
MediaTypes: 'Photo'
|
||||
|
||||
}, context, '.photoResults', {
|
||||
|
||||
showParentTitle: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'PhotoAlbum'
|
||||
|
||||
}, context, '.photoAlbumResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Book'
|
||||
|
||||
}, context, '.bookResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'AudioBook'
|
||||
|
||||
}, context, '.audioBookResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
|
||||
searchType(instance, apiClient, {
|
||||
searchTerm: value,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: true,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false,
|
||||
IncludeItemTypes: 'Playlist'
|
||||
|
||||
}, context, '.playlistResults', {
|
||||
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
});
|
||||
}
|
||||
|
||||
function searchType(instance, apiClient, query, context, section, cardOptions) {
|
||||
query.Limit = enableScrollX() ? 24 : 16;
|
||||
query.ParentId = instance.options.parentId;
|
||||
|
||||
getSearchHints(instance, apiClient, query).then(function (result) {
|
||||
populateResults(result, context, section, cardOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function populateResults(result, context, section, cardOptions) {
|
||||
section = context.querySelector(section);
|
||||
|
||||
const items = result.Items || result.SearchHints;
|
||||
|
||||
const itemsContainer = section.querySelector('.itemsContainer');
|
||||
|
||||
cardBuilder.buildCards(items, Object.assign({
|
||||
|
||||
itemsContainer: itemsContainer,
|
||||
parentContainer: section,
|
||||
shape: enableScrollX() ? 'autooverflow' : 'auto',
|
||||
scalable: true,
|
||||
overlayText: false,
|
||||
centerText: true,
|
||||
allowBottomPadding: !enableScrollX()
|
||||
|
||||
}, cardOptions || {}));
|
||||
}
|
||||
|
||||
function enableScrollX() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function replaceAll(originalString, strReplace, strWith) {
|
||||
const reg = new RegExp(strReplace, 'ig');
|
||||
return originalString.replace(reg, strWith);
|
||||
}
|
||||
|
||||
function embed(elem, instance) {
|
||||
let workingTemplate = template;
|
||||
if (!enableScrollX()) {
|
||||
workingTemplate = replaceAll(workingTemplate, 'data-horizontal="true"', 'data-horizontal="false"');
|
||||
workingTemplate = replaceAll(workingTemplate, 'itemsContainer scrollSlider', 'itemsContainer scrollSlider vertical-wrap');
|
||||
}
|
||||
|
||||
const html = globalize.translateHtml(workingTemplate, 'core');
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
elem.classList.add('searchResults');
|
||||
instance.search('');
|
||||
}
|
||||
|
||||
class SearchResults {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options.element, this);
|
||||
}
|
||||
search(value) {
|
||||
const apiClient = ServerConnections.getApiClient(this.options.serverId);
|
||||
|
||||
search(this, apiClient, this.options.element, value);
|
||||
}
|
||||
destroy() {
|
||||
const options = this.options;
|
||||
if (options) {
|
||||
options.element.classList.remove('searchFields');
|
||||
}
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResults;
|
||||
|
||||
/* eslint-enable indent */
|
|
@ -1,145 +0,0 @@
|
|||
<div class="hide verticalSection searchSuggestions" style="text-align:center;">
|
||||
|
||||
<div>
|
||||
<h2 class="sectionTitle padded-left padded-right">${Suggestions}</h2>
|
||||
</div>
|
||||
|
||||
<div class="searchSuggestionsList padded-left padded-right">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection movieResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Movies}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection seriesResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Shows}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection episodeResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Episodes}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection sportsResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Sports}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection kidsResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Kids}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection newsResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${News}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection programResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Programs}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection videoResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Videos}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection playlistResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Playlists}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection artistResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Artists}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection albumResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Albums}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection songResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Songs}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection photoAlbumResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${HeaderPhotoAlbums}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection photoResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Photos}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection audioBookResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${HeaderAudioBooks}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection bookResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${Books}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide verticalSection peopleResults">
|
||||
<h2 class="sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right">${People}</h2>
|
||||
|
||||
<div is="emby-scroller" data-horizontal="true" data-centerfocus="card" class="padded-top-focusscale padded-bottom-focusscale">
|
||||
<div is="emby-itemscontainer" class="focuscontainer-x itemsContainer scrollSlider"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -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) {
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
<div id="searchPage" data-role="page" class="page libraryPage allLibraryPage noSecondaryNavPage" data-title="${Search}" data-backbutton="true">
|
||||
<div class="padded-left padded-right searchFields"></div>
|
||||
<div class="searchResults padded-bottom-page padded-top"></div>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -304,7 +304,7 @@ import { appRouter } from '../components/appRouter';
|
|||
defineRoute({
|
||||
alias: '/search.html',
|
||||
path: 'search.html',
|
||||
controller: 'searchpage'
|
||||
pageComponent: 'SearchPage'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue