refactor: suggestionview and genresview
This commit is contained in:
parent
13aa3c9efa
commit
17e8ccc93a
27 changed files with 1253 additions and 602 deletions
184
package-lock.json
generated
184
package-lock.json
generated
|
@ -21,6 +21,8 @@
|
||||||
"@loadable/component": "5.15.3",
|
"@loadable/component": "5.15.3",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.11.16",
|
||||||
"@mui/material": "5.13.3",
|
"@mui/material": "5.13.3",
|
||||||
|
"@tanstack/react-query": "4.29.12",
|
||||||
|
"@tanstack/react-query-devtools": "4.29.12",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
|
@ -3472,6 +3474,75 @@
|
||||||
"string.prototype.matchall": "^4.0.6"
|
"string.prototype.matchall": "^4.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/match-sorter-utils": {
|
||||||
|
"version": "8.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz",
|
||||||
|
"integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"remove-accents": "0.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kentcdodds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "4.29.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.11.tgz",
|
||||||
|
"integrity": "sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "4.29.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.12.tgz",
|
||||||
|
"integrity": "sha512-zhcN6+zF6cxprxhTHQajHGlvxgK8npnp9uLe9yaWhGc6sYcPWXzyO4raL4HomUzQOPzu3jLvkriJQ7BOrDM8vA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "4.29.11",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
|
"version": "4.29.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.12.tgz",
|
||||||
|
"integrity": "sha512-ug4YGQhMhh6QI8/sWJhjXxuvdeehxf1cyxpTifGMH5qreQ5ECHT6vzqG/aKvADQDzqLBGrF0q4wTDnRRYvvtrA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/match-sorter-utils": "^8.7.0",
|
||||||
|
"superjson": "^1.10.0",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "4.29.12",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@trysound/sax": {
|
"node_modules/@trysound/sax": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||||
|
@ -5880,6 +5951,20 @@
|
||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-anything": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "^4.1.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/copy-descriptor": {
|
"node_modules/copy-descriptor": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||||
|
@ -10460,6 +10545,17 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-what": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-Aoe8pT24sWzyoO0S2PTDyutGp9l7qYHyFtzYlC8hMLshyqV/minljBANT4f2hiS5OxnWvcKMiA5io+VaLMJ1oA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-whitespace-character": {
|
"node_modules/is-whitespace-character": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
||||||
|
@ -14466,6 +14562,11 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/remove-accents": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
|
||||||
|
},
|
||||||
"node_modules/renderkid": {
|
"node_modules/renderkid": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||||
|
@ -18396,6 +18497,17 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/superjson": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
@ -19430,6 +19542,14 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -22627,6 +22747,38 @@
|
||||||
"string.prototype.matchall": "^4.0.6"
|
"string.prototype.matchall": "^4.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tanstack/match-sorter-utils": {
|
||||||
|
"version": "8.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz",
|
||||||
|
"integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==",
|
||||||
|
"requires": {
|
||||||
|
"remove-accents": "0.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/query-core": {
|
||||||
|
"version": "4.29.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.11.tgz",
|
||||||
|
"integrity": "sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ=="
|
||||||
|
},
|
||||||
|
"@tanstack/react-query": {
|
||||||
|
"version": "4.29.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.12.tgz",
|
||||||
|
"integrity": "sha512-zhcN6+zF6cxprxhTHQajHGlvxgK8npnp9uLe9yaWhGc6sYcPWXzyO4raL4HomUzQOPzu3jLvkriJQ7BOrDM8vA==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/query-core": "4.29.11",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/react-query-devtools": {
|
||||||
|
"version": "4.29.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.12.tgz",
|
||||||
|
"integrity": "sha512-ug4YGQhMhh6QI8/sWJhjXxuvdeehxf1cyxpTifGMH5qreQ5ECHT6vzqG/aKvADQDzqLBGrF0q4wTDnRRYvvtrA==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/match-sorter-utils": "^8.7.0",
|
||||||
|
"superjson": "^1.10.0",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@trysound/sax": {
|
"@trysound/sax": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||||
|
@ -24508,6 +24660,14 @@
|
||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"copy-anything": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||||
|
"requires": {
|
||||||
|
"is-what": "^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"copy-descriptor": {
|
"copy-descriptor": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||||
|
@ -27907,6 +28067,11 @@
|
||||||
"get-intrinsic": "^1.1.1"
|
"get-intrinsic": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-what": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-Aoe8pT24sWzyoO0S2PTDyutGp9l7qYHyFtzYlC8hMLshyqV/minljBANT4f2hiS5OxnWvcKMiA5io+VaLMJ1oA=="
|
||||||
|
},
|
||||||
"is-whitespace-character": {
|
"is-whitespace-character": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
||||||
|
@ -30779,6 +30944,11 @@
|
||||||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"remove-accents": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
|
||||||
|
},
|
||||||
"renderkid": {
|
"renderkid": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||||
|
@ -33876,6 +34046,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"superjson": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==",
|
||||||
|
"requires": {
|
||||||
|
"copy-anything": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
@ -34626,6 +34804,12 @@
|
||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
|
@ -77,6 +77,8 @@
|
||||||
"@loadable/component": "5.15.3",
|
"@loadable/component": "5.15.3",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.11.16",
|
||||||
"@mui/material": "5.13.3",
|
"@mui/material": "5.13.3",
|
||||||
|
"@tanstack/react-query": "4.29.12",
|
||||||
|
"@tanstack/react-query-devtools": "4.29.12",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
import { History } from '@remix-run/router';
|
import { History } from '@remix-run/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
import StableApp from './apps/stable/App';
|
import StableApp from './apps/stable/App';
|
||||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
import { HistoryRouter } from './components/router/HistoryRouter';
|
||||||
|
@ -9,21 +11,26 @@ import { WebConfigProvider } from './hooks/useWebConfig';
|
||||||
|
|
||||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const RootApp = ({ history }: { history: History }) => {
|
const RootApp = ({ history }: { history: History }) => {
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApiProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<WebConfigProvider>
|
<ApiProvider>
|
||||||
<HistoryRouter history={history}>
|
<WebConfigProvider>
|
||||||
{
|
<HistoryRouter history={history}>
|
||||||
layoutMode === 'experimental' ?
|
{
|
||||||
<ExperimentalApp /> :
|
layoutMode === 'experimental' ?
|
||||||
<StableApp />
|
<ExperimentalApp /> :
|
||||||
}
|
<StableApp />
|
||||||
</HistoryRouter>
|
}
|
||||||
</WebConfigProvider>
|
</HistoryRouter>
|
||||||
</ApiProvider>
|
</WebConfigProvider>
|
||||||
|
</ApiProvider>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useGetGenres } from 'hooks/useFetchItems';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import GenresSectionContainer from './GenresSectionContainer';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
|
||||||
|
interface GenresItemsContainerProps {
|
||||||
|
parentId?: string | null;
|
||||||
|
collectionType?: CollectionType;
|
||||||
|
itemType: BaseItemKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
|
parentId,
|
||||||
|
collectionType,
|
||||||
|
itemType
|
||||||
|
}) => {
|
||||||
|
const { isLoading, data: genresResult } = useGetGenres(
|
||||||
|
parentId,
|
||||||
|
itemType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!genresResult?.Items?.length ? (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
genresResult?.Items
|
||||||
|
&& genresResult?.Items.map((genre) => (
|
||||||
|
<GenresSectionContainer
|
||||||
|
key={genre.Id}
|
||||||
|
collectionType={collectionType}
|
||||||
|
parentId={parentId}
|
||||||
|
itemType={itemType}
|
||||||
|
genre={genre}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresItemsContainer;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { useGetItems } from 'hooks/useFetchItems';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
|
||||||
|
interface GenresSectionContainerProps {
|
||||||
|
parentId?: string | null;
|
||||||
|
collectionType?: CollectionType;
|
||||||
|
itemType: BaseItemKind;
|
||||||
|
genre: BaseItemDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
|
parentId,
|
||||||
|
collectionType,
|
||||||
|
itemType,
|
||||||
|
genre
|
||||||
|
}) => {
|
||||||
|
const getParametersOptions = () => {
|
||||||
|
return {
|
||||||
|
sortBy: [ItemSortBy.Random],
|
||||||
|
sortOrder: [SortOrder.Ascending],
|
||||||
|
includeItemTypes: [itemType],
|
||||||
|
recursive: true,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.MediaSourceCount,
|
||||||
|
ItemFields.BasicSyncInfo
|
||||||
|
],
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [ImageType.Primary],
|
||||||
|
limit: 25,
|
||||||
|
genreIds: genre.Id ? [genre.Id] : undefined,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
parentId: parentId ?? undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isLoading, data: itemsResult } = useGetItems(getParametersOptions());
|
||||||
|
|
||||||
|
const getRouteUrl = (item: BaseItemDto) => {
|
||||||
|
return appRouter.getRouteUrl(item, {
|
||||||
|
context: collectionType,
|
||||||
|
parentId: parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SectionContainer
|
||||||
|
sectionTitle={escapeHTML(genre.Name)}
|
||||||
|
items={itemsResult?.Items || []}
|
||||||
|
url={getRouteUrl(genre)}
|
||||||
|
cardOptions={{
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
shape: itemType === BaseItemKind.MusicAlbum ? 'overflowSquare' : 'overflowPortrait',
|
||||||
|
showParentTitle: itemType === BaseItemKind.MusicAlbum ? true : false,
|
||||||
|
showYear: itemType === BaseItemKind.MusicAlbum ? false : true
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresSectionContainer;
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
|
||||||
|
interface RecommendationContainerProps {
|
||||||
|
recommendation?: RecommendationDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecommendationContainer: FC<RecommendationContainerProps> = ({
|
||||||
|
recommendation = {}
|
||||||
|
}) => {
|
||||||
|
let title = '';
|
||||||
|
|
||||||
|
switch (recommendation.RecommendationType) {
|
||||||
|
case RecommendationType.SimilarToRecentlyPlayed:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationBecauseYouWatched',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.SimilarToLikedItem:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationBecauseYouLike',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
||||||
|
case RecommendationType.HasLikedDirector:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationDirectedBy',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.HasActorFromRecentlyPlayed:
|
||||||
|
case RecommendationType.HasLikedActor:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationStarring',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer
|
||||||
|
sectionTitle={escapeHTML(title)}
|
||||||
|
items={recommendation.Items || []}
|
||||||
|
cardOptions={{
|
||||||
|
shape: 'overflowPortrait',
|
||||||
|
showYear: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendationContainer;
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
|
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
||||||
|
import Scroller from 'elements/emby-scroller/Scroller';
|
||||||
|
import LinkButton from 'elements/emby-button/LinkButton';
|
||||||
|
import imageLoader from 'components/images/imageLoader';
|
||||||
|
|
||||||
|
import { CardOptions } from 'types/cardOptions';
|
||||||
|
|
||||||
|
interface SectionContainerProps {
|
||||||
|
url?: string;
|
||||||
|
sectionTitle: string;
|
||||||
|
items: BaseItemDto[];
|
||||||
|
cardOptions: CardOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionContainer: FC<SectionContainerProps> = ({
|
||||||
|
sectionTitle,
|
||||||
|
url,
|
||||||
|
items,
|
||||||
|
cardOptions
|
||||||
|
}) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const itemsContainer = element.current?.querySelector('.itemsContainer');
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: itemsContainer,
|
||||||
|
parentContainer: element.current,
|
||||||
|
|
||||||
|
...cardOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
imageLoader.lazyChildren(itemsContainer);
|
||||||
|
}, [cardOptions, items]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element} className='verticalSection hide'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards padded-left'>
|
||||||
|
{url && items.length > 5 ? (
|
||||||
|
<LinkButton
|
||||||
|
className='more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre'
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards'>
|
||||||
|
{sectionTitle}
|
||||||
|
</h2>
|
||||||
|
<span
|
||||||
|
className='material-icons chevron_right'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
</LinkButton>
|
||||||
|
) : (
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards'>
|
||||||
|
{sectionTitle}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Scroller
|
||||||
|
className='padded-top-focusscale padded-bottom-focusscale'
|
||||||
|
isMouseWheelEnabled={false}
|
||||||
|
isCenterFocusEnabled={true}
|
||||||
|
>
|
||||||
|
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
|
||||||
|
</Scroller>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionContainer;
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
|
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
||||||
|
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
||||||
|
|
||||||
|
const getSuggestionsSections = (): Sections[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'HeaderContinueWatching',
|
||||||
|
viewType: SectionsViewType.ResumeItems,
|
||||||
|
type: 'Movie',
|
||||||
|
view: SectionsView.ContinueWatchingMovies,
|
||||||
|
parametersOptions: {
|
||||||
|
includeItemTypes: [BaseItemKind.Movie]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
preferThumb: true,
|
||||||
|
shape: 'overflowBackdrop',
|
||||||
|
showYear: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderLatestMovies',
|
||||||
|
viewType: SectionsViewType.LatestMedia,
|
||||||
|
type: 'Movie',
|
||||||
|
view: SectionsView.LatestMovies,
|
||||||
|
parametersOptions: {
|
||||||
|
includeItemTypes: [BaseItemKind.Movie]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
shape: 'overflowPortrait',
|
||||||
|
showYear: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderContinueWatching',
|
||||||
|
viewType: SectionsViewType.ResumeItems,
|
||||||
|
type: 'Episode',
|
||||||
|
view: SectionsView.ContinueWatchingEpisode,
|
||||||
|
parametersOptions: {
|
||||||
|
includeItemTypes: [BaseItemKind.Episode]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
shape: 'overflowBackdrop',
|
||||||
|
preferThumb: true,
|
||||||
|
inheritThumb:
|
||||||
|
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||||
|
showYear: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderLatestEpisodes',
|
||||||
|
viewType: SectionsViewType.LatestMedia,
|
||||||
|
type: 'Episode',
|
||||||
|
view: SectionsView.LatestEpisode,
|
||||||
|
parametersOptions: {
|
||||||
|
includeItemTypes: [BaseItemKind.Episode]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
shape: 'overflowBackdrop',
|
||||||
|
preferThumb: true,
|
||||||
|
showSeriesYear: true,
|
||||||
|
showParentTitle: true,
|
||||||
|
overlayText: false,
|
||||||
|
showUnplayedIndicator: false,
|
||||||
|
showChildCountIndicator: true,
|
||||||
|
lazy: true,
|
||||||
|
lines: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NextUp',
|
||||||
|
viewType: SectionsViewType.NextUp,
|
||||||
|
type: 'nextup',
|
||||||
|
view: SectionsView.NextUp,
|
||||||
|
cardOptions: {
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
shape: 'overflowBackdrop',
|
||||||
|
preferThumb: true,
|
||||||
|
inheritThumb:
|
||||||
|
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
||||||
|
showParentTitle: true,
|
||||||
|
overlayText: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderLatestMusic',
|
||||||
|
viewType: SectionsViewType.LatestMedia,
|
||||||
|
type: 'Audio',
|
||||||
|
view: SectionsView.LatestMusic,
|
||||||
|
parametersOptions: {
|
||||||
|
includeItemTypes: [BaseItemKind.Audio]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
showUnplayedIndicator: false,
|
||||||
|
shape: 'overflowSquare',
|
||||||
|
showTitle: true,
|
||||||
|
showParentTitle: true,
|
||||||
|
lazy: true,
|
||||||
|
centerText: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
cardLayout: false,
|
||||||
|
coverImage: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderRecentlyPlayed',
|
||||||
|
type: 'Audio',
|
||||||
|
view: SectionsView.RecentlyPlayedMusic,
|
||||||
|
parametersOptions: {
|
||||||
|
sortBy: [ItemSortBy.DatePlayed],
|
||||||
|
sortOrder: [SortOrder.Descending],
|
||||||
|
includeItemTypes: [BaseItemKind.Audio]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
showUnplayedIndicator: false,
|
||||||
|
shape: 'overflowSquare',
|
||||||
|
showTitle: true,
|
||||||
|
showParentTitle: true,
|
||||||
|
action: 'instantmix',
|
||||||
|
lazy: true,
|
||||||
|
centerText: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
cardLayout: false,
|
||||||
|
coverImage: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HeaderFrequentlyPlayed',
|
||||||
|
type: 'Audio',
|
||||||
|
view: SectionsView.FrequentlyPlayedMusic,
|
||||||
|
parametersOptions: {
|
||||||
|
sortBy: [ItemSortBy.PlayCount],
|
||||||
|
sortOrder: [SortOrder.Descending],
|
||||||
|
includeItemTypes: [BaseItemKind.Audio]
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
showUnplayedIndicator: false,
|
||||||
|
shape: 'overflowSquare',
|
||||||
|
showTitle: true,
|
||||||
|
showParentTitle: true,
|
||||||
|
action: 'instantmix',
|
||||||
|
lazy: true,
|
||||||
|
centerText: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
cardLayout: false,
|
||||||
|
coverImage: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SuggestionsItemsContainerProps {
|
||||||
|
parentId?: string | null;
|
||||||
|
sectionsView: SectionsView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionsItemsContainer: FC<SuggestionsItemsContainerProps> = ({
|
||||||
|
parentId,
|
||||||
|
sectionsView
|
||||||
|
}) => {
|
||||||
|
const suggestionsSections = getSuggestionsSections();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{suggestionsSections
|
||||||
|
.filter((section) => sectionsView.includes(section.view))
|
||||||
|
.map((section) => (
|
||||||
|
<SuggestionsSectionContainer
|
||||||
|
key={section.view}
|
||||||
|
parentId={parentId}
|
||||||
|
section={section}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionsItemsContainer;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useGetItemsBySectionType } from 'hooks/useFetchItems';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
|
||||||
|
import { Sections } from 'types/suggestionsSections';
|
||||||
|
|
||||||
|
interface SuggestionsSectionContainerProps {
|
||||||
|
parentId?: string | null;
|
||||||
|
section: Sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
|
||||||
|
parentId,
|
||||||
|
section
|
||||||
|
}) => {
|
||||||
|
const getRouteUrl = () => {
|
||||||
|
return appRouter.getRouteUrl('list', {
|
||||||
|
serverId: window.ApiClient.serverId(),
|
||||||
|
itemTypes: section.type,
|
||||||
|
parentId: parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isLoading, data: items } = useGetItemsBySectionType(
|
||||||
|
section,
|
||||||
|
parentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer
|
||||||
|
sectionTitle={globalize.translate(section.name)}
|
||||||
|
items={items || []}
|
||||||
|
url={getRouteUrl()}
|
||||||
|
cardOptions={{
|
||||||
|
...section.cardOptions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionsSectionContainer;
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
const getBasekey = useCallback(() => {
|
||||||
return 'collections';
|
return 'collections';
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -18,7 +18,7 @@ const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ViewItemsContainer
|
||||||
topParentId={topParentId}
|
topParentId={parentId}
|
||||||
isBtnFilterEnabled={false}
|
isBtnFilterEnabled={false}
|
||||||
isBtnNewCollectionEnabled={true}
|
isBtnNewCollectionEnabled={true}
|
||||||
isAlphaPickerEnabled={false}
|
isAlphaPickerEnabled={false}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
const getBasekey = useCallback(() => {
|
||||||
return 'favorites';
|
return 'favorites';
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -18,7 +18,7 @@ const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ViewItemsContainer
|
||||||
topParentId={topParentId}
|
topParentId={parentId}
|
||||||
getBasekey={getBasekey}
|
getBasekey={getBasekey}
|
||||||
getItemTypes={getItemTypes}
|
getItemTypes={getItemTypes}
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
|
|
@ -1,41 +1,15 @@
|
||||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import GenresItemsContainer from '../../components/library/GenresItemsContainer';
|
||||||
import loading from '../../../../components/loading/loading';
|
import { LibraryViewProps } from 'types/library';
|
||||||
import GenresItemsContainer from '../../../../components/common/GenresItemsContainer';
|
import { CollectionType } from 'types/collectionType';
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
|
||||||
|
|
||||||
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|
||||||
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
|
||||||
|
|
||||||
const reloadItems = useCallback(() => {
|
|
||||||
loading.show();
|
|
||||||
window.ApiClient.getGenres(
|
|
||||||
window.ApiClient.getCurrentUserId(),
|
|
||||||
{
|
|
||||||
SortBy: 'SortName',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Recursive: true,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ParentId: topParentId
|
|
||||||
}
|
|
||||||
).then((result) => {
|
|
||||||
setItemsResult(result);
|
|
||||||
loading.hide();
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[GenresView] failed to fetch genres', err);
|
|
||||||
});
|
|
||||||
}, [topParentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reloadItems();
|
|
||||||
}, [reloadItems]);
|
|
||||||
|
|
||||||
|
const GenresView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
return (
|
return (
|
||||||
<GenresItemsContainer
|
<GenresItemsContainer
|
||||||
topParentId={topParentId}
|
parentId={parentId}
|
||||||
itemsResult={itemsResult}
|
collectionType={CollectionType.Movies}
|
||||||
|
itemType={BaseItemKind.Movie}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
const getBasekey = useCallback(() => {
|
||||||
return 'movies';
|
return 'movies';
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -18,7 +18,7 @@ const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ViewItemsContainer
|
||||||
topParentId={topParentId}
|
topParentId={parentId}
|
||||||
isBtnShuffleEnabled={true}
|
isBtnShuffleEnabled={true}
|
||||||
getBasekey={getBasekey}
|
getBasekey={getBasekey}
|
||||||
getItemTypes={getItemTypes}
|
getItemTypes={getItemTypes}
|
||||||
|
|
|
@ -1,160 +1,51 @@
|
||||||
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
import React, { FC } from 'react';
|
||||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
import { useGetMovieRecommendations } from 'hooks/useFetchItems';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import RecommendationContainer from '../../components/library/RecommendationContainer';
|
||||||
|
import SuggestionsItemsContainer from '../../components/library/SuggestionsItemsContainer';
|
||||||
|
|
||||||
import layoutManager from '../../../../components/layoutManager';
|
import { LibraryViewProps } from 'types/library';
|
||||||
import loading from '../../../../components/loading/loading';
|
import { SectionsView } from 'types/suggestionsSections';
|
||||||
import dom from '../../../../scripts/dom';
|
|
||||||
import globalize from '../../../../scripts/globalize';
|
|
||||||
import RecommendationContainer from '../../../../components/common/RecommendationContainer';
|
|
||||||
import SectionContainer from '../../../../components/common/SectionContainer';
|
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
|
||||||
|
|
||||||
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
const SuggestionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
const {
|
||||||
const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
|
isLoading,
|
||||||
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
|
data: movieRecommendationsItems
|
||||||
const element = useRef<HTMLDivElement>(null);
|
} = useGetMovieRecommendations(parentId);
|
||||||
|
|
||||||
const enableScrollX = useCallback(() => {
|
if (isLoading) {
|
||||||
return !layoutManager.desktop;
|
return <Loading />;
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
const getPortraitShape = useCallback(() => {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}, [enableScrollX]);
|
|
||||||
|
|
||||||
const getThumbShape = useCallback(() => {
|
|
||||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
|
||||||
}, [enableScrollX]);
|
|
||||||
|
|
||||||
const autoFocus = useCallback((page) => {
|
|
||||||
import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
|
||||||
autoFocuser.autoFocus(page);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[SuggestionsView] failed to load data', err);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadResume = useCallback((page, userId, parentId) => {
|
|
||||||
loading.show();
|
|
||||||
const screenWidth = dom.getWindowSize().innerWidth;
|
|
||||||
const options = {
|
|
||||||
SortBy: 'DatePlayed',
|
|
||||||
SortOrder: 'Descending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Filters: 'IsResumable',
|
|
||||||
Limit: screenWidth >= 1600 ? 5 : 3,
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
CollapseBoxSetItems: false,
|
|
||||||
ParentId: parentId,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
EnableTotalRecordCount: false
|
|
||||||
};
|
|
||||||
window.ApiClient.getItems(userId, options).then(result => {
|
|
||||||
setResumeResult(result);
|
|
||||||
|
|
||||||
loading.hide();
|
|
||||||
autoFocus(page);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[SuggestionsView] failed to fetch items', err);
|
|
||||||
});
|
|
||||||
}, [autoFocus]);
|
|
||||||
|
|
||||||
const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => {
|
|
||||||
const options = {
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Limit: 18,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ParentId: parentId,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
EnableTotalRecordCount: false
|
|
||||||
};
|
|
||||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => {
|
|
||||||
setLatestItems(items);
|
|
||||||
|
|
||||||
autoFocus(page);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[SuggestionsView] failed to fetch latest items', err);
|
|
||||||
});
|
|
||||||
}, [autoFocus]);
|
|
||||||
|
|
||||||
const loadSuggestions = useCallback((page, userId) => {
|
|
||||||
const screenWidth = dom.getWindowSize().innerWidth;
|
|
||||||
let itemLimit = 5;
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
itemLimit = 8;
|
|
||||||
} else if (screenWidth >= 1200) {
|
|
||||||
itemLimit = 6;
|
|
||||||
}
|
|
||||||
const url = window.ApiClient.getUrl('Movies/Recommendations', {
|
|
||||||
userId: userId,
|
|
||||||
categoryLimit: 6,
|
|
||||||
ItemLimit: itemLimit,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
|
||||||
});
|
|
||||||
window.ApiClient.getJSON(url).then(result => {
|
|
||||||
setRecommendations(result);
|
|
||||||
|
|
||||||
autoFocus(page);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[SuggestionsView] failed to fetch recommendations', err);
|
|
||||||
});
|
|
||||||
}, [autoFocus]);
|
|
||||||
|
|
||||||
const loadSuggestionsTab = useCallback((view) => {
|
|
||||||
const parentId = topParentId;
|
|
||||||
const userId = window.ApiClient.getCurrentUserId();
|
|
||||||
loadResume(view, userId, parentId);
|
|
||||||
loadLatest(view, userId, parentId);
|
|
||||||
loadSuggestions(view, userId);
|
|
||||||
}, [loadLatest, loadResume, loadSuggestions, topParentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const page = element.current;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSuggestionsTab(page);
|
|
||||||
}, [loadSuggestionsTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element}>
|
<>
|
||||||
<SectionContainer
|
<SuggestionsItemsContainer
|
||||||
sectionTitle={globalize.translate('HeaderContinueWatching')}
|
parentId={parentId}
|
||||||
enableScrollX={enableScrollX}
|
sectionsView={[SectionsView.ContinueWatchingMovies, SectionsView.LatestMovies]}
|
||||||
items={resumeResult.Items || []}
|
|
||||||
cardOptions={{
|
|
||||||
preferThumb: true,
|
|
||||||
shape: getThumbShape(),
|
|
||||||
showYear: true
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SectionContainer
|
{!movieRecommendationsItems?.length ? (
|
||||||
sectionTitle={globalize.translate('HeaderLatestMovies')}
|
<div className='noItemsMessage centerMessage'>
|
||||||
enableScrollX={enableScrollX}
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
items={latestItems}
|
<p>
|
||||||
cardOptions={{
|
{globalize.translate(
|
||||||
shape: getPortraitShape(),
|
'MessageNoMovieSuggestionsAvailable'
|
||||||
showYear: true
|
)}
|
||||||
}}
|
</p>
|
||||||
/>
|
</div>
|
||||||
|
) : (
|
||||||
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
|
movieRecommendationsItems.map((recommendation, index) => {
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
return (
|
||||||
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
|
<RecommendationContainer
|
||||||
</div> : recommendations.map(recommendation => {
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
return <RecommendationContainer key={recommendation.CategoryId} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
key={index} // use a unique id return value may have duplicate id
|
||||||
})}
|
recommendation={recommendation}
|
||||||
</div>
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||||
import { LibraryViewProps } from '../../../../types/interface';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
const getBasekey = useCallback(() => {
|
||||||
return 'trailers';
|
return 'trailers';
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -19,7 +19,7 @@ const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ViewItemsContainer
|
||||||
topParentId={topParentId}
|
topParentId={parentId}
|
||||||
getBasekey={getBasekey}
|
getBasekey={getBasekey}
|
||||||
getItemTypes={getItemTypes}
|
getItemTypes={getItemTypes}
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
|
|
@ -1,29 +1,27 @@
|
||||||
import '../../../../elements/emby-scroller/emby-scroller';
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
import '../../../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
import '../../../../elements/emby-tabs/emby-tabs';
|
import 'elements/emby-tabs/emby-tabs';
|
||||||
import '../../../../elements/emby-button/emby-button';
|
import 'elements/emby-button/emby-button';
|
||||||
|
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
import Page from 'components/Page';
|
||||||
|
|
||||||
import Page from '../../../../components/Page';
|
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||||
import globalize from '../../../../scripts/globalize';
|
|
||||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
|
||||||
import CollectionsView from './CollectionsView';
|
import CollectionsView from './CollectionsView';
|
||||||
import FavoritesView from './FavoritesView';
|
import FavoritesView from './FavoritesView';
|
||||||
import GenresView from './GenresView';
|
import GenresView from './GenresView';
|
||||||
import MoviesView from './MoviesView';
|
import MoviesView from './MoviesView';
|
||||||
import SuggestionsView from './SuggestionsView';
|
import SuggestionsView from './SuggestionsView';
|
||||||
import TrailersView from './TrailersView';
|
import TrailersView from './TrailersView';
|
||||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
|
||||||
|
|
||||||
const Movies: FC = () => {
|
const Movies: FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [ searchParams ] = useSearchParams();
|
const [ searchParams ] = useSearchParams();
|
||||||
|
const searchParamsParentId = searchParams.get('topParentId');
|
||||||
const searchParamsTab = searchParams.get('tab');
|
const searchParamsTab = searchParams.get('tab');
|
||||||
const currentTabIndex = searchParamsTab !== null ? parseInt(searchParamsTab, 10) :
|
const currentTabIndex = searchParamsTab !== null ? parseInt(searchParamsTab, 10) :
|
||||||
getDefaultTabIndex(location.pathname, searchParams.get('topParentId'));
|
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const getTabComponent = (index: number) => {
|
const getTabComponent = (index: number) => {
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
|
@ -32,72 +30,41 @@ const Movies: FC = () => {
|
||||||
|
|
||||||
let component;
|
let component;
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
|
||||||
component = <MoviesView topParentId={searchParams.get('topParentId')} />;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
component = <SuggestionsView topParentId={searchParams.get('topParentId')} />;
|
component = <SuggestionsView parentId={searchParamsParentId} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
component = <TrailersView topParentId={searchParams.get('topParentId')} />;
|
component = <TrailersView parentId={searchParamsParentId} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
component = <FavoritesView topParentId={searchParams.get('topParentId')} />;
|
component = <FavoritesView parentId={searchParamsParentId} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
component = <CollectionsView topParentId={searchParams.get('topParentId')} />;
|
component = <CollectionsView parentId={searchParamsParentId} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
component = <GenresView topParentId={searchParams.get('topParentId')} />;
|
component = <GenresView parentId={searchParamsParentId} />;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
component = <MoviesView parentId={searchParamsParentId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const page = element.current;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!page.getAttribute('data-title')) {
|
|
||||||
const parentId = searchParams.get('topParentId');
|
|
||||||
|
|
||||||
if (parentId) {
|
|
||||||
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
|
|
||||||
page.setAttribute('data-title', item.Name as string);
|
|
||||||
libraryMenu.setTitle(item.Name);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[movies] failed to fetch library', err);
|
|
||||||
page.setAttribute('data-title', globalize.translate('Movies'));
|
|
||||||
libraryMenu.setTitle(globalize.translate('Movies'));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
page.setAttribute('data-title', globalize.translate('Movies'));
|
|
||||||
libraryMenu.setTitle(globalize.translate('Movies'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [ searchParams ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element}>
|
<Page
|
||||||
<Page
|
id='moviesPage'
|
||||||
id='moviesPage'
|
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
backDropType='movie'
|
||||||
backDropType='movie'
|
>
|
||||||
>
|
{getTabComponent(currentTabIndex)}
|
||||||
{getTabComponent(currentTabIndex)}
|
|
||||||
|
|
||||||
</Page>
|
</Page>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
import '../../elements/emby-button/emby-button';
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
|
|
||||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import escapeHTML from 'escape-html';
|
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { appRouter } from '../router/appRouter';
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
|
||||||
import layoutManager from '../layoutManager';
|
|
||||||
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
|
||||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
|
||||||
|
|
||||||
const createLinkElement = ({ className, title, href }: { className?: string, title?: string | null, href?: string }) => ({
|
|
||||||
__html: `<a
|
|
||||||
is="emby-linkbutton"
|
|
||||||
class="${className}"
|
|
||||||
href="${href}"
|
|
||||||
>
|
|
||||||
<h2 class='sectionTitle sectionTitle-cards'>
|
|
||||||
${title}
|
|
||||||
</h2>
|
|
||||||
<span class='material-icons chevron_right' aria-hidden='true'></span>
|
|
||||||
</a>`
|
|
||||||
});
|
|
||||||
|
|
||||||
interface GenresItemsContainerProps {
|
|
||||||
topParentId?: string | null;
|
|
||||||
itemsResult: BaseItemDtoQueryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
|
||||||
topParentId,
|
|
||||||
itemsResult = {}
|
|
||||||
}) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const enableScrollX = useCallback(() => {
|
|
||||||
return !layoutManager.desktop;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getPortraitShape = useCallback(() => {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}, [enableScrollX]);
|
|
||||||
|
|
||||||
const fillItemsContainer = useCallback((entry) => {
|
|
||||||
const elem = entry.target;
|
|
||||||
const id = elem.getAttribute('data-id');
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
SortBy: 'Random',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary',
|
|
||||||
Limit: 12,
|
|
||||||
GenreIds: id,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ParentId: topParentId
|
|
||||||
};
|
|
||||||
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
|
||||||
cardBuilder.buildCards(result.Items || [], {
|
|
||||||
itemsContainer: elem,
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
allowBottomPadding: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
showYear: true
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[GenresItemsContainer] failed to fetch items', err);
|
|
||||||
});
|
|
||||||
}, [getPortraitShape, topParentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const elem = element.current;
|
|
||||||
lazyLoader.lazyChildren(elem, fillItemsContainer);
|
|
||||||
}, [itemsResult.Items, fillItemsContainer]);
|
|
||||||
|
|
||||||
const items = itemsResult.Items || [];
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
{
|
|
||||||
!items.length ? (
|
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
|
||||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
|
||||||
</div>
|
|
||||||
) : items.map(item => (
|
|
||||||
<div key={item.Id} className='verticalSection'>
|
|
||||||
<div
|
|
||||||
className='sectionTitleContainer sectionTitleContainer-cards padded-left'
|
|
||||||
dangerouslySetInnerHTML={createLinkElement({
|
|
||||||
className: 'more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre',
|
|
||||||
title: escapeHTML(item.Name),
|
|
||||||
href: appRouter.getRouteUrl(item, {
|
|
||||||
context: 'movies',
|
|
||||||
parentId: topParentId
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{enableScrollX() ?
|
|
||||||
<ItemsScrollerContainerElement
|
|
||||||
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
|
||||||
dataMousewheel='false'
|
|
||||||
dataCenterfocus='true'
|
|
||||||
className='itemsContainer scrollSlider focuscontainer-x lazy'
|
|
||||||
dataId={item.Id}
|
|
||||||
/> : <ItemsContainerElement
|
|
||||||
className='itemsContainer vertical-wrap lazy padded-left padded-right'
|
|
||||||
dataId={item.Id}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GenresItemsContainer;
|
|
|
@ -1,48 +0,0 @@
|
||||||
import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import escapeHTML from 'escape-html';
|
|
||||||
import SectionContainer from './SectionContainer';
|
|
||||||
|
|
||||||
interface RecommendationContainerProps {
|
|
||||||
getPortraitShape: () => string;
|
|
||||||
enableScrollX: () => boolean;
|
|
||||||
recommendation?: RecommendationDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RecommendationContainer: FC<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }) => {
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
switch (recommendation.RecommendationType) {
|
|
||||||
case 'SimilarToRecentlyPlayed':
|
|
||||||
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SimilarToLikedItem':
|
|
||||||
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'HasDirectorFromRecentlyPlayed':
|
|
||||||
case 'HasLikedDirector':
|
|
||||||
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'HasActorFromRecentlyPlayed':
|
|
||||||
case 'HasLikedActor':
|
|
||||||
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SectionContainer
|
|
||||||
sectionTitle={escapeHTML(title)}
|
|
||||||
enableScrollX={enableScrollX}
|
|
||||||
items={recommendation.Items || []}
|
|
||||||
cardOptions={{
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
showYear: true
|
|
||||||
}}
|
|
||||||
/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecommendationContainer;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
|
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
|
||||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
|
||||||
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
|
||||||
import { CardOptions } from '../../types/interface';
|
|
||||||
|
|
||||||
interface SectionContainerProps {
|
|
||||||
sectionTitle: string;
|
|
||||||
enableScrollX: () => boolean;
|
|
||||||
items?: BaseItemDto[];
|
|
||||||
cardOptions?: CardOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SectionContainer: FC<SectionContainerProps> = ({
|
|
||||||
sectionTitle,
|
|
||||||
enableScrollX,
|
|
||||||
items = [],
|
|
||||||
cardOptions = {}
|
|
||||||
}) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cardBuilder.buildCards(items, {
|
|
||||||
itemsContainer: element.current?.querySelector('.itemsContainer'),
|
|
||||||
parentContainer: element.current?.querySelector('.verticalSection'),
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
...cardOptions
|
|
||||||
});
|
|
||||||
}, [cardOptions, enableScrollX, items]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<div className='verticalSection hide'>
|
|
||||||
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
|
||||||
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
|
||||||
{sectionTitle}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{enableScrollX() ? <ItemsScrollerContainerElement
|
|
||||||
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
|
||||||
dataMousewheel='false'
|
|
||||||
dataCenterfocus='true'
|
|
||||||
className='itemsContainer scrollSlider focuscontainer-x'
|
|
||||||
/> : <ItemsContainerElement
|
|
||||||
className='itemsContainer focuscontainer-x padded-left padded-right vertical-wrap'
|
|
||||||
/>}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SectionContainer;
|
|
|
@ -12,12 +12,13 @@ import Shuffle from './Shuffle';
|
||||||
import Sort from './Sort';
|
import Sort from './Sort';
|
||||||
import NewCollection from './NewCollection';
|
import NewCollection from './NewCollection';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import { CardOptions, ViewQuerySettings } from '../../types/interface';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
import listview from '../listview/listview';
|
import listview from '../listview/listview';
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
|
|
||||||
|
import { ViewQuerySettings } from '../../types/interface';
|
||||||
|
import { CardOptions } from '../../types/cardOptions';
|
||||||
interface ViewItemsContainerProps {
|
interface ViewItemsContainerProps {
|
||||||
topParentId: string | null;
|
topParentId: string | null;
|
||||||
isBtnShuffleEnabled?: boolean;
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ServerConnections from '../components/ServerConnections';
|
||||||
import events from '../utils/events';
|
import events from '../utils/events';
|
||||||
import { toApi } from '../utils/jellyfin-apiclient/compat';
|
import { toApi } from '../utils/jellyfin-apiclient/compat';
|
||||||
|
|
||||||
interface JellyfinApiContext {
|
export interface JellyfinApiContext {
|
||||||
__legacyApiClient__?: ApiClient
|
__legacyApiClient__?: ApiClient
|
||||||
api?: Api
|
api?: Api
|
||||||
user?: UserDto
|
user?: UserDto
|
||||||
|
|
287
src/hooks/useFetchItems.ts
Normal file
287
src/hooks/useFetchItems.ts
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
|
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
|
||||||
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
|
||||||
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
|
import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api';
|
||||||
|
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||||
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { JellyfinApiContext, useApi } from './useApi';
|
||||||
|
import { Sections, SectionsViewType } from 'types/suggestionsSections';
|
||||||
|
|
||||||
|
type ParentId = string | null | undefined;
|
||||||
|
|
||||||
|
const fetchGetItem = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
parentId: ParentId,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id && parentId) {
|
||||||
|
const response = await getUserLibraryApi(api).getItem(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
itemId: parentId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetItem = (parentId: ParentId) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['Item', parentId],
|
||||||
|
queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }),
|
||||||
|
enabled: !!parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGetItems = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
parametersOptions: ItemsApiGetItemsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
const response = await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
...parametersOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetItems = (parametersOptions: ItemsApiGetItemsRequest) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'Items',
|
||||||
|
{
|
||||||
|
...parametersOptions
|
||||||
|
}
|
||||||
|
],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetItems(currentApi, parametersOptions, { signal })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGetMovieRecommendations = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
parentId: ParentId,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
const response = await getMoviesApi(api).getMovieRecommendations(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.MediaSourceCount,
|
||||||
|
ItemFields.BasicSyncInfo
|
||||||
|
],
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
categoryLimit: 6,
|
||||||
|
itemLimit: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMovieRecommendations = (parentId: ParentId) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['MovieRecommendations', parentId],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetMovieRecommendations(currentApi, parentId, { signal }),
|
||||||
|
enabled: !!parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGetItemsBySuggestionsType = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
sections: Sections,
|
||||||
|
parentId: ParentId,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
let response;
|
||||||
|
switch (sections.viewType) {
|
||||||
|
case SectionsViewType.NextUp: {
|
||||||
|
response = (
|
||||||
|
await getTvShowsApi(api).getNextUp(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
limit: 25,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.MediaSourceCount,
|
||||||
|
ItemFields.BasicSyncInfo
|
||||||
|
],
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [
|
||||||
|
ImageType.Primary,
|
||||||
|
ImageType.Backdrop,
|
||||||
|
ImageType.Thumb
|
||||||
|
],
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
...sections.parametersOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.Items;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SectionsViewType.ResumeItems: {
|
||||||
|
response = (
|
||||||
|
await getItemsApi(api).getResumeItems(
|
||||||
|
{
|
||||||
|
userId: user?.Id,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.MediaSourceCount,
|
||||||
|
ItemFields.BasicSyncInfo
|
||||||
|
],
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [ImageType.Thumb],
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
...sections.parametersOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.Items;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SectionsViewType.LatestMedia: {
|
||||||
|
response = (
|
||||||
|
await getUserLibraryApi(api).getLatestMedia(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.MediaSourceCount,
|
||||||
|
ItemFields.BasicSyncInfo
|
||||||
|
],
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [ImageType.Primary],
|
||||||
|
...sections.parametersOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
response = (
|
||||||
|
await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
recursive: true,
|
||||||
|
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||||
|
filters: [ItemFilter.IsPlayed],
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [
|
||||||
|
ImageType.Primary,
|
||||||
|
ImageType.Backdrop,
|
||||||
|
ImageType.Thumb
|
||||||
|
],
|
||||||
|
limit: 25,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
...sections.parametersOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.Items;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetItemsBySectionType = (
|
||||||
|
sections: Sections,
|
||||||
|
parentId: ParentId
|
||||||
|
) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['ItemsBySuggestionsType', sections.view],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetItemsBySuggestionsType(
|
||||||
|
currentApi,
|
||||||
|
sections,
|
||||||
|
parentId,
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
enabled: !!sections.view
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGetGenres = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
parentId: ParentId,
|
||||||
|
itemType: BaseItemKind,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
const response = await getGenresApi(api).getGenres(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
sortBy: [ItemSortBy.SortName],
|
||||||
|
sortOrder: [SortOrder.Ascending],
|
||||||
|
includeItemTypes: [itemType],
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
parentId: parentId ?? undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetGenres = (parentId: ParentId, itemType: BaseItemKind) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['Genres', parentId],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetGenres(currentApi, parentId, itemType, { signal }),
|
||||||
|
enabled: !!parentId
|
||||||
|
});
|
||||||
|
};
|
74
src/types/cardOptions.ts
Normal file
74
src/types/cardOptions.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
|
export interface CardOptions {
|
||||||
|
itemsContainer?: HTMLElement | null;
|
||||||
|
parentContainer?: HTMLElement | null;
|
||||||
|
items?: BaseItemDto[] | null;
|
||||||
|
allowBottomPadding?: boolean;
|
||||||
|
centerText?: boolean;
|
||||||
|
coverImage?: boolean;
|
||||||
|
inheritThumb?: boolean;
|
||||||
|
overlayMoreButton?: boolean;
|
||||||
|
overlayPlayButton?: boolean;
|
||||||
|
overlayText?: boolean;
|
||||||
|
preferThumb?: boolean;
|
||||||
|
preferDisc?: boolean;
|
||||||
|
preferLogo?: boolean;
|
||||||
|
scalable?: boolean;
|
||||||
|
shape?: string | null;
|
||||||
|
lazy?: boolean;
|
||||||
|
cardLayout?: boolean | string;
|
||||||
|
showParentTitle?: boolean;
|
||||||
|
showParentTitleOrTitle?: boolean;
|
||||||
|
showAirTime?: boolean;
|
||||||
|
showAirDateTime?: boolean;
|
||||||
|
showChannelName?: boolean;
|
||||||
|
showTitle?: boolean | string;
|
||||||
|
showYear?: boolean | string;
|
||||||
|
showDetailsMenu?: boolean;
|
||||||
|
missingIndicator?: boolean;
|
||||||
|
showLocationTypeIndicator?: boolean;
|
||||||
|
showSeriesYear?: boolean;
|
||||||
|
showUnplayedIndicator?: boolean;
|
||||||
|
showChildCountIndicator?: boolean;
|
||||||
|
lines?: number;
|
||||||
|
context?: string | null;
|
||||||
|
action?: string | null;
|
||||||
|
defaultShape?: string;
|
||||||
|
indexBy?: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
showMenu?: boolean;
|
||||||
|
cardCssClass?: string | null;
|
||||||
|
cardClass?: string | null;
|
||||||
|
centerPlayButton?: boolean;
|
||||||
|
overlayInfoButton?: boolean;
|
||||||
|
autoUpdate?: boolean;
|
||||||
|
cardFooterAside?: string;
|
||||||
|
includeParentInfoInTitle?: boolean;
|
||||||
|
maxLines?: number;
|
||||||
|
overlayMarkPlayedButton?: boolean;
|
||||||
|
overlayRateButton?: boolean;
|
||||||
|
showAirEndTime?: boolean;
|
||||||
|
showCurrentProgram?: boolean;
|
||||||
|
showCurrentProgramTime?: boolean;
|
||||||
|
showItemCounts?: boolean;
|
||||||
|
showPersonRoleOrType?: boolean;
|
||||||
|
showProgressBar?: boolean;
|
||||||
|
showPremiereDate?: boolean;
|
||||||
|
showRuntime?: boolean;
|
||||||
|
showSeriesTimerTime?: boolean;
|
||||||
|
showSeriesTimerChannel?: boolean;
|
||||||
|
showSongCount?: boolean;
|
||||||
|
width?: number;
|
||||||
|
showChannelLogo?: boolean;
|
||||||
|
showLogo?: boolean;
|
||||||
|
serverId?: string;
|
||||||
|
collectionId?: string | null;
|
||||||
|
playlistId?: string | null;
|
||||||
|
defaultCardImageIcon?: string;
|
||||||
|
disableHoverMenu?: boolean;
|
||||||
|
disableIndicators?: boolean;
|
||||||
|
showGroupCount?: boolean;
|
||||||
|
containerClass?: string;
|
||||||
|
noItemsMessage?: string;
|
||||||
|
}
|
|
@ -1,19 +1,3 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
|
|
||||||
export interface Query extends ViewQuerySettings {
|
|
||||||
IncludeItemTypes?: string;
|
|
||||||
Recursive?: boolean;
|
|
||||||
Fields?: string | null;
|
|
||||||
ImageTypeLimit?: number;
|
|
||||||
EnableTotalRecordCount?: boolean;
|
|
||||||
EnableImageTypes?: string;
|
|
||||||
StartIndex?: number;
|
|
||||||
ParentId?: string | null;
|
|
||||||
IsMissing?: boolean | null;
|
|
||||||
Limit?:number;
|
|
||||||
Filters?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ViewQuerySettings {
|
export interface ViewQuerySettings {
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
showYear?: boolean;
|
showYear?: boolean;
|
||||||
|
@ -43,80 +27,3 @@ export interface ViewQuerySettings {
|
||||||
NameStartsWith?: string | null;
|
NameStartsWith?: string | null;
|
||||||
StartIndex?: number;
|
StartIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardOptions {
|
|
||||||
itemsContainer?: HTMLElement | null;
|
|
||||||
parentContainer?: HTMLElement | null;
|
|
||||||
items?: BaseItemDto[] | null;
|
|
||||||
allowBottomPadding?: boolean;
|
|
||||||
centerText?: boolean;
|
|
||||||
coverImage?: boolean;
|
|
||||||
inheritThumb?: boolean;
|
|
||||||
overlayMoreButton?: boolean;
|
|
||||||
overlayPlayButton?: boolean;
|
|
||||||
overlayText?: boolean;
|
|
||||||
preferThumb?: boolean;
|
|
||||||
preferDisc?: boolean;
|
|
||||||
preferLogo?: boolean;
|
|
||||||
scalable?: boolean;
|
|
||||||
shape?: string | null;
|
|
||||||
lazy?: boolean;
|
|
||||||
cardLayout?: boolean | string;
|
|
||||||
showParentTitle?: boolean;
|
|
||||||
showParentTitleOrTitle?: boolean;
|
|
||||||
showAirTime?: boolean;
|
|
||||||
showAirDateTime?: boolean;
|
|
||||||
showChannelName?: boolean;
|
|
||||||
showTitle?: boolean | string;
|
|
||||||
showYear?: boolean | string;
|
|
||||||
showDetailsMenu?: boolean;
|
|
||||||
missingIndicator?: boolean;
|
|
||||||
showLocationTypeIndicator?: boolean;
|
|
||||||
showSeriesYear?: boolean;
|
|
||||||
showUnplayedIndicator?: boolean;
|
|
||||||
showChildCountIndicator?: boolean;
|
|
||||||
lines?: number;
|
|
||||||
context?: string | null;
|
|
||||||
action?: string | null;
|
|
||||||
defaultShape?: string;
|
|
||||||
indexBy?: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
showMenu?: boolean;
|
|
||||||
cardCssClass?: string | null;
|
|
||||||
cardClass?: string | null;
|
|
||||||
centerPlayButton?: boolean;
|
|
||||||
overlayInfoButton?: boolean;
|
|
||||||
autoUpdate?: boolean;
|
|
||||||
cardFooterAside?: string;
|
|
||||||
includeParentInfoInTitle?: boolean;
|
|
||||||
maxLines?: number;
|
|
||||||
overlayMarkPlayedButton?: boolean;
|
|
||||||
overlayRateButton?: boolean;
|
|
||||||
showAirEndTime?: boolean;
|
|
||||||
showCurrentProgram?: boolean;
|
|
||||||
showCurrentProgramTime?: boolean;
|
|
||||||
showItemCounts?: boolean;
|
|
||||||
showPersonRoleOrType?: boolean;
|
|
||||||
showProgressBar?: boolean;
|
|
||||||
showPremiereDate?: boolean;
|
|
||||||
showRuntime?: boolean;
|
|
||||||
showSeriesTimerTime?: boolean;
|
|
||||||
showSeriesTimerChannel?: boolean;
|
|
||||||
showSongCount?: boolean;
|
|
||||||
width?: number;
|
|
||||||
showChannelLogo?: boolean;
|
|
||||||
showLogo?: boolean;
|
|
||||||
serverId?: string;
|
|
||||||
collectionId?: string | null;
|
|
||||||
playlistId?: string | null;
|
|
||||||
defaultCardImageIcon?: string;
|
|
||||||
disableHoverMenu?: boolean;
|
|
||||||
disableIndicators?: boolean;
|
|
||||||
showGroupCount?: boolean;
|
|
||||||
containerClass?: string;
|
|
||||||
noItemsMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LibraryViewProps {
|
|
||||||
topParentId: string | null;
|
|
||||||
}
|
|
||||||
|
|
45
src/types/library.ts
Normal file
45
src/types/library.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
|
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
|
||||||
|
import { VideoType } from '@jellyfin/sdk/lib/generated-client/models/video-type';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
|
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status';
|
||||||
|
|
||||||
|
export interface ParametersOptions {
|
||||||
|
sortBy?: ItemSortBy[];
|
||||||
|
sortOrder?: SortOrder[];
|
||||||
|
includeItemTypes?: BaseItemKind[];
|
||||||
|
fields?: ItemFields[];
|
||||||
|
enableImageTypes?: ImageType[];
|
||||||
|
videoTypes?: VideoType[];
|
||||||
|
seriesStatus?: SeriesStatus[];
|
||||||
|
filters?: ItemFilter[];
|
||||||
|
limit?: number;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
genres?: string[];
|
||||||
|
officialRatings?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
years?: number[];
|
||||||
|
is4K?: boolean;
|
||||||
|
isHd?: boolean;
|
||||||
|
is3D?: boolean;
|
||||||
|
hasSubtitles?: boolean;
|
||||||
|
hasTrailer?: boolean;
|
||||||
|
hasSpecialFeature?: boolean;
|
||||||
|
hasThemeSong?: boolean;
|
||||||
|
hasThemeVideo?: boolean;
|
||||||
|
parentIndexNumber?: number;
|
||||||
|
isMissing?: boolean;
|
||||||
|
isUnaired?: boolean;
|
||||||
|
startIndex?: number;
|
||||||
|
nameLessThan?: string;
|
||||||
|
nameStartsWith?: string;
|
||||||
|
collapseBoxSetItems?: boolean;
|
||||||
|
enableTotalRecordCount?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryViewProps {
|
||||||
|
parentId: string | null;
|
||||||
|
}
|
28
src/types/suggestionsSections.ts
Normal file
28
src/types/suggestionsSections.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { CardOptions } from './cardOptions';
|
||||||
|
import { ParametersOptions } from './library';
|
||||||
|
|
||||||
|
export enum SectionsViewType {
|
||||||
|
ResumeItems = 'resumeItems',
|
||||||
|
LatestMedia = 'latestMedia',
|
||||||
|
NextUp = 'nextUp',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SectionsView {
|
||||||
|
ContinueWatchingMovies = 'continuewatchingmovies',
|
||||||
|
LatestMovies = 'latestmovies',
|
||||||
|
ContinueWatchingEpisode = 'continuewatchingepisode',
|
||||||
|
LatestEpisode = 'latestepisode',
|
||||||
|
NextUp = 'nextUp',
|
||||||
|
LatestMusic = 'latestmusic',
|
||||||
|
RecentlyPlayedMusic = 'recentlyplayedmusic',
|
||||||
|
FrequentlyPlayedMusic = 'frequentlyplayedmusic',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sections {
|
||||||
|
name: string;
|
||||||
|
view: SectionsView;
|
||||||
|
type: string;
|
||||||
|
viewType?: SectionsViewType,
|
||||||
|
parametersOptions?: ParametersOptions;
|
||||||
|
cardOptions: CardOptions;
|
||||||
|
}
|
|
@ -161,12 +161,14 @@ const config = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(js|jsx)$/,
|
test: /\.(js|jsx|mjs)$/,
|
||||||
include: [
|
include: [
|
||||||
path.resolve(__dirname, 'node_modules/event-target-polyfill'),
|
path.resolve(__dirname, 'node_modules/event-target-polyfill'),
|
||||||
path.resolve(__dirname, 'node_modules/rvfc-polyfill'),
|
path.resolve(__dirname, 'node_modules/rvfc-polyfill'),
|
||||||
path.resolve(__dirname, 'node_modules/@jellyfin/sdk'),
|
path.resolve(__dirname, 'node_modules/@jellyfin/sdk'),
|
||||||
path.resolve(__dirname, 'node_modules/@remix-run/router'),
|
path.resolve(__dirname, 'node_modules/@remix-run/router'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@tanstack/query-core'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@tanstack/react-query'),
|
||||||
path.resolve(__dirname, 'node_modules/@uupaa/dynamic-import-polyfill'),
|
path.resolve(__dirname, 'node_modules/@uupaa/dynamic-import-polyfill'),
|
||||||
path.resolve(__dirname, 'node_modules/axios'),
|
path.resolve(__dirname, 'node_modules/axios'),
|
||||||
path.resolve(__dirname, 'node_modules/blurhash'),
|
path.resolve(__dirname, 'node_modules/blurhash'),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue