Merge remote-tracking branch 'upstream/master' into dashboard-activity
This commit is contained in:
commit
76597a021a
66 changed files with 2129 additions and 707 deletions
246
package-lock.json
generated
246
package-lock.json
generated
|
@ -22,6 +22,9 @@
|
|||
"@mui/icons-material": "5.11.16",
|
||||
"@mui/material": "5.13.3",
|
||||
"@mui/x-data-grid": "6.6.0",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@tanstack/react-query": "4.29.12",
|
||||
"@tanstack/react-query-devtools": "4.29.12",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.2",
|
||||
|
@ -2979,6 +2982,11 @@
|
|||
"@jridgewell/sourcemap-codec": "1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@juggle/resize-observer": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
|
@ -3399,6 +3407,35 @@
|
|||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-hook/latest": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz",
|
||||
"integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-hook/passive-layout-effect": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
|
||||
"integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-hook/resize-observer": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz",
|
||||
"integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==",
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"@react-hook/latest": "^1.0.2",
|
||||
"@react-hook/passive-layout-effect": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz",
|
||||
|
@ -3498,6 +3535,75 @@
|
|||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
|
@ -5906,6 +6012,20 @@
|
|||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||
|
@ -10486,6 +10606,17 @@
|
|||
"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": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
||||
|
@ -14492,6 +14623,11 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||
|
@ -18427,6 +18563,17 @@
|
|||
"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": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
|
@ -19461,6 +19608,14 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -22366,6 +22521,11 @@
|
|||
"@jridgewell/sourcemap-codec": "1.4.14"
|
||||
}
|
||||
},
|
||||
"@juggle/resize-observer": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||
},
|
||||
"@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
|
@ -22600,6 +22760,28 @@
|
|||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
|
||||
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw=="
|
||||
},
|
||||
"@react-hook/latest": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz",
|
||||
"integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-hook/passive-layout-effect": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
|
||||
"integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-hook/resize-observer": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz",
|
||||
"integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==",
|
||||
"requires": {
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"@react-hook/latest": "^1.0.2",
|
||||
"@react-hook/passive-layout-effect": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@remix-run/router": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz",
|
||||
|
@ -22670,6 +22852,38 @@
|
|||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
|
@ -24551,6 +24765,14 @@
|
|||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||
|
@ -27950,6 +28172,11 @@
|
|||
"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": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
|
||||
|
@ -30822,6 +31049,11 @@
|
|||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||
|
@ -33924,6 +34156,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": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
|
@ -34674,6 +34914,12 @@
|
|||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
|
@ -78,6 +78,9 @@
|
|||
"@mui/icons-material": "5.11.16",
|
||||
"@mui/material": "5.13.3",
|
||||
"@mui/x-data-grid": "6.6.0",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@tanstack/react-query": "4.29.12",
|
||||
"@tanstack/react-query-devtools": "4.29.12",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.2",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { History } from '@remix-run/router';
|
||||
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 { HistoryRouter } from './components/router/HistoryRouter';
|
||||
|
@ -9,10 +11,13 @@ import { WebConfigProvider } from './hooks/useWebConfig';
|
|||
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const RootApp = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<HistoryRouter history={history}>
|
||||
|
@ -24,6 +29,8 @@ const RootApp = ({ history }: { history: History }) => {
|
|||
</HistoryRouter>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { SyncPlayUserAccessType } from '@jellyfin/sdk/lib/generated-client/models/sync-play-user-access-type';
|
||||
import Groups from '@mui/icons-material/Groups';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { PluginType } from 'types/plugin';
|
||||
|
||||
import AppSyncPlayMenu, { ID } from './menus/SyncPlayMenu';
|
||||
|
||||
const SyncPlayButton = () => {
|
||||
const { user } = useApi();
|
||||
|
||||
const [ syncPlayMenuAnchorEl, setSyncPlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isSyncPlayMenuOpen = Boolean(syncPlayMenuAnchorEl);
|
||||
|
||||
const onSyncPlayButtonClick = useCallback((event) => {
|
||||
setSyncPlayMenuAnchorEl(event.currentTarget);
|
||||
}, [ setSyncPlayMenuAnchorEl ]);
|
||||
|
||||
const onSyncPlayMenuClose = useCallback(() => {
|
||||
setSyncPlayMenuAnchorEl(null);
|
||||
}, [ setSyncPlayMenuAnchorEl ]);
|
||||
|
||||
if (
|
||||
// SyncPlay not enabled for user
|
||||
(user?.Policy && user.Policy.SyncPlayAccess === SyncPlayUserAccessType.None)
|
||||
// SyncPlay plugin is not loaded
|
||||
|| pluginManager.ofType(PluginType.SyncPlay).length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={globalize.translate('ButtonSyncPlay')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('ButtonSyncPlay')}
|
||||
aria-controls={ID}
|
||||
aria-haspopup='true'
|
||||
onClick={onSyncPlayButtonClick}
|
||||
color='inherit'
|
||||
>
|
||||
<Groups />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<AppSyncPlayMenu
|
||||
open={isSyncPlayMenuOpen}
|
||||
anchorEl={syncPlayMenuAnchorEl}
|
||||
onMenuClose={onSyncPlayMenuClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncPlayButton;
|
|
@ -16,6 +16,7 @@ import AppTabs from '../tabs/AppTabs';
|
|||
import { isDrawerPath } from '../drawers/AppDrawer';
|
||||
import UserMenuButton from './UserMenuButton';
|
||||
import RemotePlayButton from './RemotePlayButton';
|
||||
import SyncPlayButton from './SyncPlayButton';
|
||||
|
||||
interface AppToolbarProps {
|
||||
isDrawerOpen: boolean
|
||||
|
@ -90,6 +91,7 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
|||
{isUserLoggedIn && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
import type { GroupInfoDto } from '@jellyfin/sdk/lib/generated-client/models/group-info-dto';
|
||||
import { SyncPlayUserAccessType } from '@jellyfin/sdk/lib/generated-client/models/sync-play-user-access-type';
|
||||
import { getSyncPlayApi } from '@jellyfin/sdk/lib/utils/api/sync-play-api';
|
||||
import GroupAdd from '@mui/icons-material/GroupAdd';
|
||||
import PersonAdd from '@mui/icons-material/PersonAdd';
|
||||
import PersonOff from '@mui/icons-material/PersonOff';
|
||||
import PersonRemove from '@mui/icons-material/PersonRemove';
|
||||
import PlayCircle from '@mui/icons-material/PlayCircle';
|
||||
import StopCircle from '@mui/icons-material/StopCircle';
|
||||
import Tune from '@mui/icons-material/Tune';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { PluginType } from 'types/plugin';
|
||||
import Events from 'utils/events';
|
||||
|
||||
export const ID = 'app-sync-play-menu';
|
||||
|
||||
interface SyncPlayMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
}
|
||||
|
||||
interface SyncPlayInstance {
|
||||
Manager: {
|
||||
getGroupInfo: () => GroupInfoDto | null | undefined
|
||||
getTimeSyncCore: () => object
|
||||
isPlaybackActive: () => boolean
|
||||
isPlaylistEmpty: () => boolean
|
||||
haltGroupPlayback: (apiClient: ApiClient) => void
|
||||
resumeGroupPlayback: (apiClient: ApiClient) => void
|
||||
}
|
||||
}
|
||||
|
||||
const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||
anchorEl,
|
||||
open,
|
||||
onMenuClose
|
||||
}) => {
|
||||
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
|
||||
const { __legacyApiClient__, api, user } = useApi();
|
||||
const [ groups, setGroups ] = useState<GroupInfoDto[]>([]);
|
||||
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
|
||||
const isSyncPlayEnabled = Boolean(currentGroup);
|
||||
|
||||
useEffect(() => {
|
||||
setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGroups = async () => {
|
||||
if (api) {
|
||||
setGroups((await getSyncPlayApi(api).syncPlayGetGroups()).data);
|
||||
}
|
||||
};
|
||||
|
||||
fetchGroups()
|
||||
.catch(err => {
|
||||
console.error('[SyncPlayMenu] unable to fetch SyncPlay groups', err);
|
||||
});
|
||||
}, [ api ]);
|
||||
|
||||
const onGroupAddClick = useCallback(() => {
|
||||
if (api && user) {
|
||||
getSyncPlayApi(api)
|
||||
.syncPlayCreateGroup({
|
||||
newGroupRequestDto: {
|
||||
GroupName: globalize.translate('SyncPlayGroupDefaultTitle', user.Name)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[SyncPlayMenu] failed to create a SyncPlay group', err);
|
||||
});
|
||||
|
||||
onMenuClose();
|
||||
}
|
||||
}, [ api, onMenuClose, user ]);
|
||||
|
||||
const onGroupLeaveClick = useCallback(() => {
|
||||
if (api) {
|
||||
getSyncPlayApi(api)
|
||||
.syncPlayLeaveGroup()
|
||||
.catch(err => {
|
||||
console.error('[SyncPlayMenu] failed to leave SyncPlay group', err);
|
||||
});
|
||||
|
||||
onMenuClose();
|
||||
}
|
||||
}, [ api, onMenuClose ]);
|
||||
|
||||
const onGroupJoinClick = useCallback((GroupId: string) => {
|
||||
if (api) {
|
||||
getSyncPlayApi(api)
|
||||
.syncPlayJoinGroup({
|
||||
joinGroupRequestDto: {
|
||||
GroupId
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[SyncPlayMenu] failed to join SyncPlay group', err);
|
||||
});
|
||||
|
||||
onMenuClose();
|
||||
}
|
||||
}, [ api, onMenuClose ]);
|
||||
|
||||
const onGroupSettingsClick = useCallback(async () => {
|
||||
if (!syncPlay) return;
|
||||
|
||||
// TODO: Rewrite settings UI
|
||||
const SyncPlaySettingsEditor = (await import('../../../../../plugins/syncPlay/ui/settings/SettingsEditor')).default;
|
||||
new SyncPlaySettingsEditor(
|
||||
__legacyApiClient__,
|
||||
syncPlay.Manager.getTimeSyncCore(),
|
||||
{
|
||||
groupInfo: currentGroup
|
||||
})
|
||||
.embed()
|
||||
.catch(err => {
|
||||
if (err) {
|
||||
console.error('[SyncPlayMenu] Error creating SyncPlay settings editor', err);
|
||||
}
|
||||
});
|
||||
|
||||
onMenuClose();
|
||||
}, [ __legacyApiClient__, currentGroup, onMenuClose, syncPlay ]);
|
||||
|
||||
const onStartGroupPlaybackClick = useCallback(() => {
|
||||
if (__legacyApiClient__) {
|
||||
syncPlay?.Manager.resumeGroupPlayback(__legacyApiClient__);
|
||||
onMenuClose();
|
||||
}
|
||||
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
|
||||
|
||||
const onStopGroupPlaybackClick = useCallback(() => {
|
||||
if (__legacyApiClient__) {
|
||||
syncPlay?.Manager.haltGroupPlayback(__legacyApiClient__);
|
||||
onMenuClose();
|
||||
}
|
||||
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
|
||||
|
||||
const updateSyncPlayGroup = useCallback((_e, enabled) => {
|
||||
if (syncPlay && enabled) {
|
||||
setCurrentGroup(syncPlay.Manager.getGroupInfo() ?? undefined);
|
||||
} else {
|
||||
setCurrentGroup(undefined);
|
||||
}
|
||||
}, [ syncPlay ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!syncPlay) return;
|
||||
|
||||
Events.on(syncPlay.Manager, 'enabled', updateSyncPlayGroup);
|
||||
|
||||
return () => {
|
||||
Events.off(syncPlay.Manager, 'enabled', updateSyncPlayGroup);
|
||||
};
|
||||
}, [ updateSyncPlayGroup, syncPlay ]);
|
||||
|
||||
const menuItems = [];
|
||||
if (isSyncPlayEnabled) {
|
||||
if (!syncPlay?.Manager.isPlaylistEmpty() && !syncPlay?.Manager.isPlaybackActive()) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key='sync-play-start-playback'
|
||||
onClick={onStartGroupPlaybackClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LabelSyncPlayResumePlayback')} />
|
||||
</MenuItem>
|
||||
);
|
||||
} else if (syncPlay?.Manager.isPlaybackActive()) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key='sync-play-stop-playback'
|
||||
onClick={onStopGroupPlaybackClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<StopCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LabelSyncPlayHaltPlayback')} />
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key='sync-play-settings'
|
||||
onClick={onGroupSettingsClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Tune />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={globalize.translate('Settings')}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
menuItems.push(
|
||||
<Divider key='sync-play-controls-divider' />
|
||||
);
|
||||
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key='sync-play-exit'
|
||||
onClick={onGroupLeaveClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PersonRemove />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={globalize.translate('LabelSyncPlayLeaveGroup')}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
} else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
||||
menuItems.push(
|
||||
<MenuItem key='sync-play-unavailable' disabled>
|
||||
<ListItemIcon>
|
||||
<PersonOff />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LabelSyncPlayNoGroups')} />
|
||||
</MenuItem>
|
||||
);
|
||||
} else {
|
||||
if (groups.length > 0) {
|
||||
groups.forEach(group => {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={group.GroupId}
|
||||
// Since we are looping over groups there is no good way to avoid creating a new function here
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => group.GroupId && onGroupJoinClick(group.GroupId)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PersonAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={group.GroupName}
|
||||
secondary={group.Participants?.join(', ')}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
menuItems.push(
|
||||
<Divider key='sync-play-groups-divider' />
|
||||
);
|
||||
}
|
||||
|
||||
if (user?.Policy?.SyncPlayAccess === SyncPlayUserAccessType.CreateAndJoinGroups) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key='sync-play-new-group'
|
||||
onClick={onGroupAddClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<GroupAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LabelSyncPlayNewGroupDescription')} />
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MenuListProps = isSyncPlayEnabled ? {
|
||||
'aria-labelledby': 'sync-play-active-subheader',
|
||||
subheader: (
|
||||
<ListSubheader component='div' id='sync-play-active-subheader'>
|
||||
{currentGroup?.GroupName}
|
||||
</ListSubheader>
|
||||
)
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
id={ID}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={onMenuClose}
|
||||
MenuListProps={MenuListProps}
|
||||
>
|
||||
{menuItems}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncPlayMenu;
|
|
@ -0,0 +1,51 @@
|
|||
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(
|
||||
itemType,
|
||||
parentId
|
||||
);
|
||||
|
||||
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?.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,66 @@
|
|||
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,
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
|
||||
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
||||
return 'collections';
|
||||
}, []);
|
||||
|
@ -18,7 +18,7 @@ const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
|
||||
return (
|
||||
<ViewItemsContainer
|
||||
topParentId={topParentId}
|
||||
topParentId={parentId}
|
||||
isBtnFilterEnabled={false}
|
||||
isBtnNewCollectionEnabled={true}
|
||||
isAlphaPickerEnabled={false}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
|
||||
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
||||
return 'favorites';
|
||||
}, []);
|
||||
|
@ -18,7 +18,7 @@ const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
|
||||
return (
|
||||
<ViewItemsContainer
|
||||
topParentId={topParentId}
|
||||
topParentId={parentId}
|
||||
getBasekey={getBasekey}
|
||||
getItemTypes={getItemTypes}
|
||||
getNoItemsMessage={getNoItemsMessage}
|
||||
|
|
|
@ -1,41 +1,15 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import GenresItemsContainer from '../../../../components/common/GenresItemsContainer';
|
||||
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]);
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import GenresItemsContainer from '../../components/library/GenresItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
const GenresView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
return (
|
||||
<GenresItemsContainer
|
||||
topParentId={topParentId}
|
||||
itemsResult={itemsResult}
|
||||
parentId={parentId}
|
||||
collectionType={CollectionType.Movies}
|
||||
itemType={BaseItemKind.Movie}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
|
||||
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
||||
return 'movies';
|
||||
}, []);
|
||||
|
@ -18,7 +18,7 @@ const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
|
||||
return (
|
||||
<ViewItemsContainer
|
||||
topParentId={topParentId}
|
||||
topParentId={parentId}
|
||||
isBtnShuffleEnabled={true}
|
||||
getBasekey={getBasekey}
|
||||
getItemTypes={getItemTypes}
|
||||
|
|
|
@ -1,160 +1,51 @@
|
|||
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { FC } 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 loading from '../../../../components/loading/loading';
|
||||
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';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
import { SectionsView } from 'types/suggestionsSections';
|
||||
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
||||
const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
|
||||
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const {
|
||||
isLoading,
|
||||
data: movieRecommendationsItems
|
||||
} = useGetMovieRecommendations(parentId);
|
||||
|
||||
const enableScrollX = useCallback(() => {
|
||||
return !layoutManager.desktop;
|
||||
}, []);
|
||||
|
||||
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;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
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 (
|
||||
<div ref={element}>
|
||||
<SectionContainer
|
||||
sectionTitle={globalize.translate('HeaderContinueWatching')}
|
||||
enableScrollX={enableScrollX}
|
||||
items={resumeResult.Items || []}
|
||||
cardOptions={{
|
||||
preferThumb: true,
|
||||
shape: getThumbShape(),
|
||||
showYear: true
|
||||
}}
|
||||
<>
|
||||
<SuggestionsItemsContainer
|
||||
parentId={parentId}
|
||||
sectionsView={[SectionsView.ContinueWatchingMovies, SectionsView.LatestMovies]}
|
||||
/>
|
||||
|
||||
<SectionContainer
|
||||
sectionTitle={globalize.translate('HeaderLatestMovies')}
|
||||
enableScrollX={enableScrollX}
|
||||
items={latestItems}
|
||||
cardOptions={{
|
||||
shape: getPortraitShape(),
|
||||
showYear: true
|
||||
}}
|
||||
/>
|
||||
|
||||
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
|
||||
{!movieRecommendationsItems?.length ? (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
|
||||
</div> : recommendations.map(recommendation => {
|
||||
return <RecommendationContainer key={recommendation.CategoryId} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
||||
})}
|
||||
<p>
|
||||
{globalize.translate(
|
||||
'MessageNoMovieSuggestionsAvailable'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
movieRecommendationsItems.map((recommendation, index) => {
|
||||
return (
|
||||
<RecommendationContainer
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||
recommendation={recommendation}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from 'types/library';
|
||||
|
||||
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
||||
return 'trailers';
|
||||
}, []);
|
||||
|
@ -19,7 +19,7 @@ const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
|
||||
return (
|
||||
<ViewItemsContainer
|
||||
topParentId={topParentId}
|
||||
topParentId={parentId}
|
||||
getBasekey={getBasekey}
|
||||
getItemTypes={getItemTypes}
|
||||
getNoItemsMessage={getNoItemsMessage}
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import '../../../../elements/emby-scroller/emby-scroller';
|
||||
import '../../../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import 'elements/emby-scroller/emby-scroller';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-tabs/emby-tabs';
|
||||
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 Page from 'components/Page';
|
||||
|
||||
import Page from '../../../../components/Page';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||
import CollectionsView from './CollectionsView';
|
||||
import FavoritesView from './FavoritesView';
|
||||
import GenresView from './GenresView';
|
||||
import MoviesView from './MoviesView';
|
||||
import SuggestionsView from './SuggestionsView';
|
||||
import TrailersView from './TrailersView';
|
||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||
|
||||
const Movies: FC = () => {
|
||||
const location = useLocation();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const searchParamsParentId = searchParams.get('topParentId');
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const currentTabIndex = searchParamsTab !== null ? parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, searchParams.get('topParentId'));
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
getDefaultTabIndex(location.pathname, searchParamsParentId);
|
||||
|
||||
const getTabComponent = (index: number) => {
|
||||
if (index == null) {
|
||||
|
@ -32,63 +30,33 @@ const Movies: FC = () => {
|
|||
|
||||
let component;
|
||||
switch (index) {
|
||||
case 0:
|
||||
component = <MoviesView topParentId={searchParams.get('topParentId')} />;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
component = <SuggestionsView topParentId={searchParams.get('topParentId')} />;
|
||||
component = <SuggestionsView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
component = <TrailersView topParentId={searchParams.get('topParentId')} />;
|
||||
component = <TrailersView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
component = <FavoritesView topParentId={searchParams.get('topParentId')} />;
|
||||
component = <FavoritesView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
component = <CollectionsView topParentId={searchParams.get('topParentId')} />;
|
||||
component = <CollectionsView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
component = <GenresView topParentId={searchParams.get('topParentId')} />;
|
||||
component = <GenresView parentId={searchParamsParentId} />;
|
||||
break;
|
||||
default:
|
||||
component = <MoviesView parentId={searchParamsParentId} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div ref={element}>
|
||||
<Page
|
||||
id='moviesPage'
|
||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
|
@ -97,7 +65,6 @@ const Movies: FC = () => {
|
|||
{getTabComponent(currentTabIndex)}
|
||||
|
||||
</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,14 @@ import Shuffle from './Shuffle';
|
|||
import Sort from './Sort';
|
||||
import NewCollection from './NewCollection';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { CardOptions, ViewQuerySettings } from '../../types/interface';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import listview from '../listview/listview';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
import { CardOptions } from '../../types/cardOptions';
|
||||
|
||||
interface ViewItemsContainerProps {
|
||||
topParentId: string | null;
|
||||
isBtnShuffleEnabled?: boolean;
|
||||
|
|
|
@ -34,7 +34,9 @@
|
|||
<div class="readOnlyContent">
|
||||
<div is="emby-collapse" title="${HeaderDeveloperInfo}">
|
||||
<div class="collapseContent">
|
||||
<p id="developer"></p>
|
||||
<p>${LabelDeveloper}: <span id="developer"></span></p>
|
||||
<p>${LabelRepositoryName}: <span id="repositoryName"></span></p>
|
||||
<p>${LabelRepositoryUrl}: <span id="repositoryUrl"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -64,6 +64,16 @@ function renderPackage(pkg, installedPlugins, page) {
|
|||
|
||||
$('#description', page).text(pkg.description);
|
||||
$('#developer', page).text(pkg.owner);
|
||||
// This is a hack; the repository name and URL should be part of the global values
|
||||
// for the plugin, not each individual version. So we just use the top (latest)
|
||||
// version to get this information. If it's missing (no versions), then say so.
|
||||
if (pkg.versions.length) {
|
||||
$('#repositoryName', page).text(pkg.versions[0].repositoryName);
|
||||
$('#repositoryUrl', page).text(pkg.versions[0].repositoryUrl);
|
||||
} else {
|
||||
$('#repositoryName', page).text(globalize.translate('Unknown'));
|
||||
$('#repositoryUrl', page).text(globalize.translate('Unknown'));
|
||||
}
|
||||
|
||||
if (installedPlugin) {
|
||||
const currentVersionText = globalize.translate('MessageYouHaveVersionInstalled', '<strong>' + installedPlugin.Version + '</strong>');
|
||||
|
@ -80,7 +90,7 @@ function alertText(options) {
|
|||
}
|
||||
|
||||
function performInstallation(page, name, guid, version) {
|
||||
const developer = $('#developer', page).html().toLowerCase();
|
||||
const repositoryUrl = $('#repositoryUrl', page).html().toLowerCase();
|
||||
|
||||
const alertCallback = function () {
|
||||
loading.show();
|
||||
|
@ -93,7 +103,9 @@ function performInstallation(page, name, guid, version) {
|
|||
});
|
||||
};
|
||||
|
||||
if (developer !== 'jellyfin') {
|
||||
// Check the repository URL for the official Jellyfin repository domain, or
|
||||
// present the warning for 3rd party plugins.
|
||||
if (!repositoryUrl.startsWith('https://repo.jellyfin.org/')) {
|
||||
loading.hide();
|
||||
let msg = globalize.translate('MessagePluginInstallDisclaimer');
|
||||
msg += '<br/>';
|
||||
|
|
|
@ -2,6 +2,7 @@ import loading from '../../../../components/loading/loading';
|
|||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import dialogHelper from '../../../../components/dialogHelper/dialogHelper';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-checkbox/emby-checkbox';
|
||||
|
@ -166,14 +167,36 @@ export default function(view) {
|
|||
dialog.querySelector('.newPluginForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
|
||||
const repositoryUrl = dialog.querySelector('#txtRepositoryUrl').value.toLowerCase();
|
||||
|
||||
const alertCallback = function () {
|
||||
repositories.push({
|
||||
Name: dialog.querySelector('#txtRepositoryName').value,
|
||||
Url: dialog.querySelector('#txtRepositoryUrl').value,
|
||||
Enabled: true
|
||||
});
|
||||
|
||||
saveList(view);
|
||||
dialogHelper.close(dialog);
|
||||
};
|
||||
|
||||
// Check the repository URL for the official Jellyfin repository domain, or
|
||||
// present the warning for 3rd party plugins.
|
||||
if (!repositoryUrl.startsWith('https://repo.jellyfin.org/')) {
|
||||
let msg = globalize.translate('MessageRepositoryInstallDisclaimer');
|
||||
msg += '<br/>';
|
||||
msg += '<br/>';
|
||||
msg += globalize.translate('PleaseConfirmRepositoryInstallation');
|
||||
|
||||
confirm(msg, globalize.translate('HeaderConfirmRepositoryInstallation')).then(function () {
|
||||
alertCallback();
|
||||
}).catch(() => {
|
||||
console.debug('repository not installed');
|
||||
dialogHelper.close(dialog);
|
||||
});
|
||||
} else {
|
||||
alertCallback();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ enum Direction {
|
|||
}
|
||||
|
||||
interface ScrollButtonsProps {
|
||||
scrollRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
scrollerFactoryRef: React.MutableRefObject<scrollerFactory | null>;
|
||||
scrollState: {
|
||||
scrollSize: number;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import useElementSize from 'hooks/useElementSize';
|
||||
import layoutManager from '../../components/layoutManager';
|
||||
import dom from '../../scripts/dom';
|
||||
import browser from '../../scripts/browser';
|
||||
|
@ -32,14 +33,14 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
isAllowNativeSmoothScrollEnabled,
|
||||
children
|
||||
}) => {
|
||||
const [scrollRef, size] = useElementSize();
|
||||
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const [scrollState, setScrollState] = useState({
|
||||
scrollSize: 0,
|
||||
scrollSize: size.width,
|
||||
scrollPos: 0,
|
||||
scrollWidth: 0
|
||||
});
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const scrollerFactoryRef = useRef<scrollerFactory | null>(null);
|
||||
|
||||
const getScrollSlider = useCallback(() => {
|
||||
|
@ -125,7 +126,7 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
});
|
||||
}, [getScrollPosition, getScrollSize, getScrollWidth]);
|
||||
|
||||
const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => {
|
||||
const initCenterFocus = useCallback((elem, scrollerInstance: scrollerFactory) => {
|
||||
dom.addEventListener(elem, 'focus', function (e: FocusEvent) {
|
||||
const focused = focusManager.focusableParent(e.target);
|
||||
if (focused) {
|
||||
|
@ -150,15 +151,10 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
}, [scrollerFactoryRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElement = scrollRef.current as HTMLDivElement;
|
||||
|
||||
const horizontal = isHorizontalEnabled !== false;
|
||||
const scrollbuttons = isScrollButtonsEnabled !== false;
|
||||
const mousewheel = isMouseWheelEnabled !== false;
|
||||
|
||||
const slider = scrollerElement.querySelector('.scrollSlider');
|
||||
|
||||
const scrollFrame = scrollerElement;
|
||||
const enableScrollButtons = layoutManager.desktop && horizontal && scrollbuttons;
|
||||
|
||||
const options = {
|
||||
|
@ -166,7 +162,7 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
mouseDragging: 1,
|
||||
mouseWheel: mousewheel,
|
||||
touchDragging: 1,
|
||||
slidee: slider,
|
||||
slidee: scrollRef.current?.querySelector('.scrollSlider'),
|
||||
scrollBy: 200,
|
||||
speed: horizontal ? 270 : 240,
|
||||
elasticBounds: 1,
|
||||
|
@ -183,12 +179,12 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
};
|
||||
|
||||
// If just inserted it might not have any height yet - yes this is a hack
|
||||
scrollerFactoryRef.current = new scrollerFactory(scrollFrame, options);
|
||||
scrollerFactoryRef.current = new scrollerFactory(scrollRef.current, options);
|
||||
scrollerFactoryRef.current.init();
|
||||
scrollerFactoryRef.current.reload();
|
||||
|
||||
if (layoutManager.tv && isCenterFocusEnabled) {
|
||||
initCenterFocus(scrollerElement, scrollerFactoryRef.current);
|
||||
initCenterFocus(scrollRef.current, scrollerFactoryRef.current);
|
||||
}
|
||||
|
||||
if (enableScrollButtons) {
|
||||
|
@ -200,9 +196,8 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
}
|
||||
|
||||
return () => {
|
||||
const scrollerInstance = scrollerFactoryRef.current;
|
||||
if (scrollerInstance) {
|
||||
scrollerInstance.destroy();
|
||||
if (scrollerFactoryRef.current) {
|
||||
scrollerFactoryRef.current.destroy();
|
||||
scrollerFactoryRef.current = null;
|
||||
}
|
||||
|
||||
|
@ -223,7 +218,8 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
isScrollEventEnabled,
|
||||
isSkipFocusWhenVisibleEnabled,
|
||||
onScroll,
|
||||
removeScrollEventListener
|
||||
removeScrollEventListener,
|
||||
scrollRef
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -231,7 +227,6 @@ const Scroller: FC<ScrollerProps> = ({
|
|||
{
|
||||
showControls && scrollState.scrollWidth > scrollState.scrollSize + 20
|
||||
&& <ScrollButtons
|
||||
scrollRef={scrollRef}
|
||||
scrollerFactoryRef={scrollerFactoryRef}
|
||||
scrollState={scrollState}
|
||||
/>
|
||||
|
|
|
@ -382,6 +382,8 @@ EmbySliderPrototype.attachedCallback = function () {
|
|||
} else {
|
||||
startInterval(this);
|
||||
}
|
||||
|
||||
updateValues.call(this);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -230,6 +230,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sliderBubbleText {
|
||||
|
|
|
@ -7,7 +7,7 @@ import ServerConnections from '../components/ServerConnections';
|
|||
import events from '../utils/events';
|
||||
import { toApi } from '../utils/jellyfin-apiclient/compat';
|
||||
|
||||
interface JellyfinApiContext {
|
||||
export interface JellyfinApiContext {
|
||||
__legacyApiClient__?: ApiClient
|
||||
api?: Api
|
||||
user?: UserDto
|
||||
|
|
25
src/hooks/useElementSize.ts
Normal file
25
src/hooks/useElementSize.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react';
|
||||
import useResizeObserver from '@react-hook/resize-observer';
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export default function useElementSize<
|
||||
T extends HTMLElement = HTMLDivElement
|
||||
>(): [MutableRefObject<T | null>, Size] {
|
||||
const target = useRef<T | null>(null);
|
||||
const [size, setSize] = useState<Size>({
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
target.current && setSize(target.current.getBoundingClientRect());
|
||||
}, [target]);
|
||||
|
||||
useResizeObserver(target, (entry) => setSize(entry.contentRect));
|
||||
|
||||
return [target, size];
|
||||
}
|
286
src/hooks/useFetchItems.ts
Normal file
286
src/hooks/useFetchItems.ts
Normal file
|
@ -0,0 +1,286 @@
|
|||
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';
|
||||
|
||||
const fetchGetItem = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId?: string | null,
|
||||
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?: string | null) => {
|
||||
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 }),
|
||||
cacheTime: parametersOptions.sortBy?.includes(ItemSortBy.Random) ? 0 : undefined
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetMovieRecommendations = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
parentId?: string | null,
|
||||
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?: string | null) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['MovieRecommendations', parentId],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetMovieRecommendations(currentApi, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetItemsBySuggestionsType = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
sections: Sections,
|
||||
parentId?: string | null,
|
||||
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?: string | null
|
||||
) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ItemsBySuggestionsType', sections.view],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetItemsBySuggestionsType(
|
||||
currentApi,
|
||||
sections,
|
||||
parentId,
|
||||
{ signal }
|
||||
),
|
||||
enabled: !!sections.view
|
||||
});
|
||||
};
|
||||
|
||||
const fetchGetGenres = async (
|
||||
currentApi: JellyfinApiContext,
|
||||
itemType: BaseItemKind,
|
||||
parentId?: string | null,
|
||||
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 = (itemType: BaseItemKind, parentId?: string | null) => {
|
||||
const currentApi = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['Genres', parentId],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchGetGenres(currentApi, itemType, parentId, { signal }),
|
||||
enabled: !!parentId
|
||||
});
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import DOMPurify from 'dompurify';
|
||||
|
||||
import browser from '../../scripts/browser';
|
||||
import { appHost } from '../../components/apphost';
|
||||
import loading from '../../components/loading/loading';
|
||||
|
@ -1535,7 +1537,8 @@ export class HtmlVideoPlayer {
|
|||
}
|
||||
|
||||
if (selectedTrackEvent && selectedTrackEvent.Text) {
|
||||
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
|
||||
subtitleTextElement.innerHTML = DOMPurify.sanitize(
|
||||
normalizeTrackEventText(selectedTrackEvent.Text, true));
|
||||
subtitleTextElement.classList.remove('hide');
|
||||
} else {
|
||||
subtitleTextElement.classList.add('hide');
|
||||
|
|
|
@ -121,7 +121,7 @@ function canPlayAudioFormat(format) {
|
|||
|
||||
typeString = 'audio/ogg; codecs="opus"';
|
||||
} else if (format === 'alac') {
|
||||
if (browser.iOS || browser.osx) {
|
||||
if (browser.iOS || browser.osx && browser.safari) {
|
||||
return true;
|
||||
}
|
||||
} else if (format === 'mp2') {
|
||||
|
|
|
@ -1684,11 +1684,11 @@
|
|||
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Extras часта маюць такое ж убудаванае імя, што і бацькоўскі, пазначце гэта, каб у любым выпадку выкарыстоўваць для іх убудаваныя загалоўкі.",
|
||||
"HeaderDummyChapter": "Выявы раздзела",
|
||||
"LabelDummyChapterDuration": "Інтэрвал",
|
||||
"LabelDummyChapterDurationHelp": "Інтэрвал вымання выявы главы ў секундах.",
|
||||
"LabelDummyChapterDurationHelp": "Інтэрвал паміж малюнкамі раздзелаў. Усталюйце 0, каб адключыць стварэнне вобразаў раздзелаў. Змена гэтай опцыі не паўплывае на існуючыя выявы раздзелаў.",
|
||||
"LabelDummyChapterCount": "Ліміт",
|
||||
"LabelDummyChapterCountHelp": "Максімальная колькасць выяваў раздзелаў, якія будуць выняты для кожнага медыяфайла.",
|
||||
"LabelChapterImageResolution": "Дазвол",
|
||||
"LabelChapterImageResolutionHelp": "Дазвол вынятых малюнкаў раздзелаў.",
|
||||
"LabelChapterImageResolutionHelp": "Разрозненне створаных малюнкаў раздзелаў. Змена гэтай опцыі не паўплывае на існуючыя выявы раздзелаў.",
|
||||
"ResolutionMatchSource": "Супадзенне з крыніцай",
|
||||
"SecondarySubtitles": "Дадатковыя субтытры",
|
||||
"SubtitleBlack": "Чорны",
|
||||
|
@ -1719,5 +1719,10 @@
|
|||
"MenuOpen": "Адкрыць меню",
|
||||
"MenuClose": "Зачыніць меню",
|
||||
"AllowCollectionManagement": "Дазволіць гэтаму карыстальніку кіраваць калекцыямі",
|
||||
"UserMenu": "Меню карыстальніка"
|
||||
"UserMenu": "Меню карыстальніка",
|
||||
"PasswordRequiredForAdmin": "Для ўліковых запісаў адміністратара патрабуецца пароль.",
|
||||
"GetThePlugin": "Атрымаць плагін",
|
||||
"LabelSyncPlayNoGroups": "Няма даступных груп",
|
||||
"Notifications": "Апавяшчэнні",
|
||||
"NotificationsMovedMessage": "Функцыянальнасць апавяшчэнняў перанесена ў плагін Webhook."
|
||||
}
|
||||
|
|
|
@ -696,7 +696,7 @@
|
|||
"MessagePlayAccessRestricted": "Přehrávání tohoto obsahu je aktuálně omezeno. Další informace získáte od správce serveru.",
|
||||
"MessagePleaseEnsureInternetMetadata": "Prosím zkontrolujte, zda máte povoleno stahování metadat z internetu.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Pro konfiguraci zásuvného modulu se přihlaste přímo na lokální server.",
|
||||
"MessagePluginInstallDisclaimer": "Zásuvné moduly vytvořené členy komunity jsou skvělým způsobem, jak si zlepšit prožitek pomocí dalších funkcí. Před instalací se prosím seznamte se všemi dopady, které mohou doplňky na server mít, např.: pomalejší skenování knihovny, delší zpracování na pozadí nebo snížená stabilita systému.",
|
||||
"MessagePluginInstallDisclaimer": "UPOZORNĚNÍ: Instalace zásuvného modulu třetí strany má určitá rizika. Může obsahovat nestabilní nebo zákeřný kód, a může se kdykoliv změnit. Instalujte zásuvné moduly jen těch autorů, kterým důvěřujete, a mějte na vědomi, jaké potenciální dopady to může mít, včetně kontaktování externích služeb, delšího skenování knihovny, nebo dalších procesů na pozadí.",
|
||||
"MessageReenableUser": "Viz níže pro znovuzapnutí",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "Z vaší knihovny budou odstraněny následující zdroje médií",
|
||||
"MessageUnableToConnectToServer": "Nejsme schopni se připojit k vybranému serveru právě teď. Prosím, ujistěte se, že je spuštěn a zkuste to znovu.",
|
||||
|
@ -1737,5 +1737,11 @@
|
|||
"GetThePlugin": "Získat zásuvný modul",
|
||||
"Notifications": "Oznámení",
|
||||
"NotificationsMovedMessage": "Funkce oznámení se přesunula do zásuvného modulu Webhook.",
|
||||
"PasswordRequiredForAdmin": "Administrátorské účty musejí mít nastaveno heslo."
|
||||
"PasswordRequiredForAdmin": "Administrátorské účty musejí mít nastaveno heslo.",
|
||||
"PleaseConfirmRepositoryInstallation": "Kliknutím na OK potvrďte, ze jste si přečetli text uvedený výše, a že chcete pokračovat v instalaci repozitáře zásuvného modulu.",
|
||||
"Unknown": "Neznámý",
|
||||
"LabelSyncPlayNoGroups": "Žádné skupiny nejsou k dispozici",
|
||||
"HeaderConfirmRepositoryInstallation": "Potvrdit instalaci repozitáře zásuvných modulů",
|
||||
"LabelDeveloper": "Vývojář",
|
||||
"MessageRepositoryInstallDisclaimer": "UPOZORNĚNÍ: Instalace zásuvného modulu třetí strany má určitá rizika. Může obsahovat nestabilní nebo zákeřný kód, a může se kdykoliv změnit. Instalujte zásuvné moduly jen těch autorů, kterým důvěřujete."
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"AllowRemoteAccessHelp": "Hvis ikke markeret, vil alle forbindelser udefra blive blokeret.",
|
||||
"AllowedRemoteAddressesHelp": "Komma separeret liste over IP-adresser eller IP/rundsendings-adresser til netværk som har tilladelse til at forbinde udefra. Hvis dette felt er tomt, er alle eksterne adresser tilladt.",
|
||||
"Anytime": "Altid",
|
||||
"AroundTime": "Omkring",
|
||||
"AroundTime": "Cirka [0]",
|
||||
"AsManyAsPossible": "Så mange som muligt",
|
||||
"AspectRatio": "Billedformat",
|
||||
"Audio": "Lyd",
|
||||
|
|
|
@ -1508,7 +1508,7 @@
|
|||
"DisablePlugin": "Deaktivieren",
|
||||
"EnablePlugin": "Aktivieren",
|
||||
"Framerate": "Bildrate",
|
||||
"DirectPlayHelp": "Die Quelldatei ist vollständig mit diesem Client kompatibel, und die Sitzung empfängt die Datei ohne Änderungen.",
|
||||
"DirectPlayHelp": "Die Quelldatei ist vollständig mit diesem Client kompatibel und die Sitzung empfängt die Datei ohne Änderungen.",
|
||||
"HeaderContinueReading": "Weiterlesen",
|
||||
"EnableGamepadHelp": "Auf Eingaben aller verbundenen Controller hören. (Erfordert: 'TV'-Anzeigemodus)",
|
||||
"LabelEnableGamepad": "Gamepad aktivieren",
|
||||
|
@ -1700,7 +1700,7 @@
|
|||
"HeaderDummyChapter": "Kapitel Bilder",
|
||||
"HeaderRecordingMetadataSaving": "Aufzeichnung von Metadaten",
|
||||
"LabelDummyChapterDuration": "Intervall",
|
||||
"LabelDummyChapterDurationHelp": "Das Intervall für die Extraktion des Kapitelbildes in Sekunden.",
|
||||
"LabelDummyChapterDurationHelp": "Das Intervall zwischen Dummy-Kapiteln. Auf 0 setzen, um die Erzeugung von Dummy-Kapiteln zu deaktivieren. Eine Änderung dieses Wertes hat keine Auswirkung auf bestehende Dummy-Kapitel.",
|
||||
"LabelDummyChapterCount": "Limit",
|
||||
"LabelDummyChapterCountHelp": "Die maximale Anzahl von Kapitelbildern, die für jede Mediendatei extrahiert werden.",
|
||||
"LabelChapterImageResolution": "Auflösung",
|
||||
|
@ -1729,5 +1729,19 @@
|
|||
"MenuClose": "Menü schließen",
|
||||
"UserMenu": "Benutzermenü",
|
||||
"Studio": "Studio",
|
||||
"AllowCollectionManagement": "Dieser Benutzer darf Sammlungen verwalten"
|
||||
"AllowCollectionManagement": "Dieser Benutzer darf Sammlungen verwalten",
|
||||
"PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.",
|
||||
"LabelEnableLUFSScan": "LUFS scan aktivieren",
|
||||
"LabelSyncPlayNoGroups": "Keine Gruppen verfügbar",
|
||||
"LabelEnableLUFSScanHelp": "LUFS scan für Musik aktivieren (Dies erfodert mehr Zeit und Ressourcen).",
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.",
|
||||
"EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).",
|
||||
"EnableAudioNormalization": "Audionormalisierung",
|
||||
"GetThePlugin": "Plugin laden",
|
||||
"HeaderConfirmRepositoryInstallation": "Plugin-Repository-Installation bestätigen",
|
||||
"LabelDeveloper": "Entwickler",
|
||||
"MessageRepositoryInstallDisclaimer": "WARNUNG: Eine Drittanbieterquelle für Plugins kann instabilen oder schadhaften Code beinhalten und kann sich zu jedem Zeitpunkt ändern. Bitte installiere nur Quellen von Autoren, denen du vertraust.",
|
||||
"PleaseConfirmRepositoryInstallation": "Bitte klicke auf OK um zu bestätigen, dass du den obigen Text gelesen hast und mit Plugin-Quellen-Installation fortfahren willst.",
|
||||
"Unknown": "Unbekannt"
|
||||
}
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"MinutesBefore": "minutes before",
|
||||
"MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the 'Refresh' button, or do bulk refreshes using the 'Metadata Manager'.",
|
||||
"MetadataManager": "Metadata Manager",
|
||||
"MessagePluginInstallDisclaimer": "Plugins built by community members are a great way to enhance your experience with additional features and benefits. Before installing, please be aware of the effects they may have on your server, such as longer library scans, additional background processing, and decreased system stability.",
|
||||
"MessagePluginInstallDisclaimer": "WARNING: Installing a third party plugin carries risks. It may contain unstable or malicious code, and may change at any time. Only install plugins from authors that you trust, and please be aware of the potential effects it may have, including external service queries, longer library scans, or additional background processing.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "To configure this plugin please sign in to your local server directly.",
|
||||
"MessagePleaseWait": "Please wait. This may take a minute.",
|
||||
"MessagePleaseEnsureInternetMetadata": "Please ensure downloading of internet metadata is enabled.",
|
||||
|
@ -1517,7 +1517,7 @@
|
|||
"MessagePlaybackError": "There was an error playing this file on your Google Cast receiver.",
|
||||
"MessageChromecastConnectionError": "Your Google Cast receiver is unable to contact the Jellyfin server. Please check the connection and try again.",
|
||||
"Framerate": "Framerate",
|
||||
"DirectPlayHelp": "The source file is entirely compatible with this client, and the session is receiving the file without modifications.",
|
||||
"DirectPlayHelp": "The source file is entirely compatible with this client and the session is receiving the file without modifications.",
|
||||
"EnableGamepadHelp": "Listen for input from any connected controllers. (Requires: 'TV' Display Mode)",
|
||||
"LabelEnableGamepad": "Enable Gamepad",
|
||||
"Controls": "Controls",
|
||||
|
@ -1737,5 +1737,11 @@
|
|||
"GetThePlugin": "Get the Plugin",
|
||||
"Notifications": "Notifications",
|
||||
"NotificationsMovedMessage": "The notifications functionality has moved to the Webhook plugin.",
|
||||
"PasswordRequiredForAdmin": "A password is required for admin accounts."
|
||||
"PasswordRequiredForAdmin": "A password is required for admin accounts.",
|
||||
"LabelSyncPlayNoGroups": "No groups available",
|
||||
"HeaderConfirmRepositoryInstallation": "Confirm Plugin Repository Installation",
|
||||
"LabelDeveloper": "Developer",
|
||||
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
|
||||
"PleaseConfirmRepositoryInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin repository installation.",
|
||||
"Unknown": "Unknown"
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
"Director": "Director",
|
||||
"Directors": "Directors",
|
||||
"DirectPlaying": "Direct playing",
|
||||
"DirectPlayHelp": "The source file is entirely compatible with this client, and the session is receiving the file without modifications.",
|
||||
"DirectPlayHelp": "The source file is entirely compatible with this client and the session is receiving the file without modifications.",
|
||||
"DirectStreamHelp1": "The video stream is compatible with the device, but has an incompatible audio format (DTS, Dolby TrueHD, etc.) or number of audio channels. The video stream will be repackaged losslessly on the fly before being sent to the device. Only the audio stream will be transcoded.",
|
||||
"DirectStreamHelp2": "Power consumed by direct streaming usually depends on the audio profile. Only the video stream is lossless.",
|
||||
"DirectStreaming": "Direct streaming",
|
||||
|
@ -337,6 +337,7 @@
|
|||
"HeaderCodecProfileHelp": "Codec profiles indicate the limitations of a device when playing specific codecs. If a limitation applies then the media will be transcoded, even if the codec is configured for direct playback.",
|
||||
"HeaderConfigureRemoteAccess": "Set up Remote Access",
|
||||
"HeaderConfirmPluginInstallation": "Confirm Plugin Installation",
|
||||
"HeaderConfirmRepositoryInstallation": "Confirm Plugin Repository Installation",
|
||||
"HeaderConfirmProfileDeletion": "Confirm Profile Deletion",
|
||||
"HeaderConfirmRevokeApiKey": "Revoke API Key",
|
||||
"HeaderConnectionFailure": "Connection Failure",
|
||||
|
@ -613,6 +614,7 @@
|
|||
"LabelDefaultUser": "Default user",
|
||||
"LabelDefaultUserHelp": "Determine which user library should be displayed on connected devices. This can be overridden for each device using profiles.",
|
||||
"LabelDeinterlaceMethod": "Deinterlacing method",
|
||||
"LabelDeveloper": "Developer",
|
||||
"LabelDeviceDescription": "Device description",
|
||||
"LabelDidlMode": "DIDL mode",
|
||||
"LabelDisableCustomCss": "Disable custom CSS code for theming/branding provided from the server.",
|
||||
|
@ -908,6 +910,7 @@
|
|||
"LabelSyncPlayLeaveGroupDescription": "Disable SyncPlay",
|
||||
"LabelSyncPlayNewGroup": "New group",
|
||||
"LabelSyncPlayNewGroupDescription": "Create a new group",
|
||||
"LabelSyncPlayNoGroups": "No groups available",
|
||||
"LabelSyncPlayPlaybackDiff": "Playback time difference",
|
||||
"LabelSyncPlayResumePlayback": "Resume local playback",
|
||||
"LabelSyncPlayResumePlaybackDescription": "Join back group playback",
|
||||
|
@ -1109,11 +1112,12 @@
|
|||
"MessagePleaseEnsureInternetMetadata": "Please ensure downloading of internet metadata is enabled.",
|
||||
"MessagePleaseWait": "Please wait. This may take a minute.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "To set up this plugin please sign in to your local server directly.",
|
||||
"MessagePluginInstallDisclaimer": "Plugins built by community members are a great way to enhance your experience with additional features and benefits. Before installing, please be aware of the effects they may have on your server, such as longer library scans, additional background processing, and decreased system stability.",
|
||||
"MessagePluginInstallDisclaimer": "WARNING: Installing a third party plugin carries risks. It may contain unstable or malicious code, and may change at any time. Only install plugins from authors that you trust, and please be aware of the potential effects it may have, including external service queries, longer library scans, or additional background processing.",
|
||||
"MessagePluginInstalled": "The plugin has been successfully installed. The server will need to be restarted for changes to take effect.",
|
||||
"MessagePluginInstallError": "An error occurred while installing the plugin.",
|
||||
"MessageReenableUser": "See below to reenable",
|
||||
"MessageRenameMediaFolder": "Renaming a media library will cause all metadata to be lost, proceed with caution.",
|
||||
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
|
||||
"MessageSent": "Message sent.",
|
||||
"MessageSyncPlayCreateGroupDenied": "Permission required to create a group.",
|
||||
"MessageSyncPlayDisabled": "SyncPlay disabled.",
|
||||
|
@ -1328,6 +1332,7 @@
|
|||
"PlayNextEpisodeAutomatically": "Play next episode automatically",
|
||||
"PleaseAddAtLeastOneFolder": "Please add at least one folder to this library by clicking the '+' button in 'Folders' section.",
|
||||
"PleaseConfirmPluginInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin installation.",
|
||||
"PleaseConfirmRepositoryInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin repository installation.",
|
||||
"PleaseEnterNameOrId": "Please enter a name or an external ID.",
|
||||
"PleaseRestartServerName": "Please restart Jellyfin on {0}.",
|
||||
"PleaseSelectTwoItems": "Please select at least two items.",
|
||||
|
@ -1720,5 +1725,6 @@
|
|||
"MediaInfoDvBlSignalCompatibilityId": "DV bl signal compatibility id",
|
||||
"Unreleased": "Not yet released",
|
||||
"LabelTonemappingMode": "Tone mapping mode",
|
||||
"TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode."
|
||||
"TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode.",
|
||||
"Unknown": "Unknown"
|
||||
}
|
||||
|
|
|
@ -1695,7 +1695,7 @@
|
|||
"SaveRecordingImages": "Guardar grabación de imágenes EPG",
|
||||
"SaveRecordingImagesHelp": "Guardar imágenes del proveedor de listados EPG junto con los archivos multimedia.",
|
||||
"LabelDummyChapterDuration": "Intervalo",
|
||||
"LabelDummyChapterDurationHelp": "Intervalo de extracción de imágenes de los capítulos en segundos.",
|
||||
"LabelDummyChapterDurationHelp": "Intervalo entre capítulos dummy. Dejar a 0 para deshabilitar la generación de capítulos dummy. Cambiar esta opción no tendrá efecto para capítulos dummy ya existentes.",
|
||||
"HeaderDummyChapter": "Imágenes de capítulos",
|
||||
"LabelDummyChapterCount": "Límite",
|
||||
"Featurette": "Reportaje extra",
|
||||
|
@ -1704,7 +1704,7 @@
|
|||
"SecondarySubtitles": "Subtítulos secundarios",
|
||||
"LabelParallelImageEncodingLimit": "Límite de codificación de imágenes en paralelo",
|
||||
"LabelEnableAudioVbr": "Habilitar codificación de audio VBR",
|
||||
"LabelChapterImageResolutionHelp": "Resolución de las imágenes de los capítulos extraídos.",
|
||||
"LabelChapterImageResolutionHelp": "Resolución de las imágenes extraídas de los capítulos. Cambiar esta opción no tendrá efecto sobre capítulos dummy ya existentes.",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNames": "Prefiere títulos incrustados sobre nombres de archivo para extras",
|
||||
"LabelDummyChapterCountHelp": "Número máximo de imágenes de capítulos que se extraerán para cada archivo multimedia.",
|
||||
"LabelEnableAudioVbrHelp": "La tasa de bits variable ofrece una mejor relación entre calidad y tasa de bits promedio, pero en algunos casos raros puede causar problemas de almacenamiento de búfer y compatibilidad.",
|
||||
|
@ -1729,5 +1729,14 @@
|
|||
"MenuClose": "Cerrar Menú",
|
||||
"UserMenu": "Menú de Usuario",
|
||||
"Studio": "Estudio",
|
||||
"AllowCollectionManagement": "Permitir que este usuario administre colecciones"
|
||||
"AllowCollectionManagement": "Permitir que este usuario administre colecciones",
|
||||
"EnableAudioNormalizationHelp": "La normalización de audio añadirá una ganancia constante para mantener la media en el nivel deseado (-18dB).",
|
||||
"LabelEnableLUFSScan": "Habilitar escaneo LUFS",
|
||||
"PasswordRequiredForAdmin": "Se requiere contraseña para las cuentas de administrador.",
|
||||
"GetThePlugin": "Obtener el Plugin",
|
||||
"NotificationsMovedMessage": "La funcionalidad de notificaciones se ha movido al plugin Webhook.",
|
||||
"LabelSyncPlayNoGroups": "No hay grupos disponibles",
|
||||
"Notifications": "Notificaciones",
|
||||
"EnableAudioNormalization": "Normalización de audio",
|
||||
"LabelEnableLUFSScanHelp": "Habilitar escaneo LUFS para música (Esto tardará más y consumirá más recursos)."
|
||||
}
|
||||
|
|
|
@ -1505,7 +1505,7 @@
|
|||
"EnableAutoCast": "تنظیم به عنوان پیشفرض",
|
||||
"DisablePlugin": "غیرفعال کردن",
|
||||
"EnablePlugin": "فعال کردن",
|
||||
"DirectPlayHelp": "فایل منبع کاملاً با این سرویس گیرنده سازگار است و جلسه بدون تغییر پرونده در حال دریافت فایل است.",
|
||||
"DirectPlayHelp": "فایل منبع کاملاً با این سرویس گیرنده سازگار است و جلسه در حال دریافت فایل بدون تغییر است.",
|
||||
"DeleteDevicesConfirmation": "آیا مطمئن هستید که می خواهید همه دستگاه ها را حذف کنید؟ تمام جلسات دیگر از سیستم خارج می شوند. بار دیگر که کاربر وارد سیستم شود ، دستگاه ها دوباره ظاهر می شوند.",
|
||||
"Bwdif": "BWDIF",
|
||||
"ButtonUseQuickConnect": "از اتصال سریع استفاده کنید",
|
||||
|
@ -1601,5 +1601,6 @@
|
|||
"HeaderPerformance": "کارایی",
|
||||
"IgnoreDtsHelp": "غیر فعال کردن این گزینه ممکن است برخی اشکالات را رفع کند، مثل نبودن صدا بر روی کانال هایی که جریان صدا و تصویر جداگانه دارند.",
|
||||
"LabelDummyChapterDurationHelp": "وقفه استخراج تصاویر فصل به ثانیه.",
|
||||
"HeaderDummyChapter": "تصاویر فصل"
|
||||
"HeaderDummyChapter": "تصاویر فصل",
|
||||
"EnableAudioNormalization": "معمول سازی صوت"
|
||||
}
|
||||
|
|
|
@ -1259,7 +1259,7 @@
|
|||
"Framerate": "Virkistystaajuus",
|
||||
"DisablePlugin": "Poista käytöstä",
|
||||
"EnablePlugin": "Ota käyttöön",
|
||||
"DirectPlayHelp": "Lähdetiedosto on täysin yhteensopiva päätesovelluksen kanssa ja istunto vastaanottaa tiedoston ilman muuntoa.",
|
||||
"DirectPlayHelp": "Lähdetiedosto on täysin yhteensopiva tämän päätesovelluksen kanssa ja istunto vastaanottaa tiedoston ilman muuntoa.",
|
||||
"LabelMaxStreamingBitrateHelp": "Määritä suoratoiston enimmäisbittinopeus.",
|
||||
"LabelMinAudiobookResumeHelp": "Kohteita pidetään toistamattomina, jos toisto keskeytetään ennen tätä aikaa.",
|
||||
"LabelMaxStreamingBitrate": "Suoratoiston enimmäislaatu",
|
||||
|
@ -1375,7 +1375,7 @@
|
|||
"MessageSent": "Viesti lähetetty.",
|
||||
"MessagePluginInstallError": "Asennettaessa lisäosaa tapahtui virhe.",
|
||||
"MessagePluginInstalled": "Lisäosan asennus onnistui. Palvelin on käynnistettävä uudelleen, jotta muutokset tulevat voimaan.",
|
||||
"MessagePluginInstallDisclaimer": "Yhteisön kehittämät lisäosat ovat mainio tapa parantaa käyttökokemustasi lisäominaisuuksilla. Huomioi ennen lisäosien asennusta, että ne voivat vaikuttaa palvelimen toimintaan, mm. pidentämällä kirjastopäivityksiä ja lisäämällä taustakuormitusta, sekä aiheuttaa järjestelmän epävakautta.",
|
||||
"MessagePluginInstallDisclaimer": "VAROITUS: Ulkopuolisten tahojen kehittämien lisäosien asennus on aina riskialtista ja ne voivat sisältää epävakaata tai haitallista koodia, ja muuttua koska tahansa. Asenna lisäosia vain kehittäjiltä, joihin luotat ja ymmärrä niiden mahdolliset vaikutukset, kuten yhteydenotot ulkoisiin palveluihin, pidentyvät kirjastotarkistukset tai erilaiset taustaprosessit.",
|
||||
"MessagePlayAccessRestricted": "Tämän sisällön toistoa on rajoitettu. Lisätietoja saat palvelimen ylläpidolta.",
|
||||
"MessagePasswordResetForUsers": "Seuraavien käyttäjien salasanat on tyhjennetty ja he voivat nyt kirjautua käyttäen tyhjennykseen käytettäjä Helppo PIN -koodeja.",
|
||||
"MessageNoTrailersFound": "Asenna trailerit-kanava parantaaksesi elokuvakokemusta lisäämällä internet-trailereiden kirjasto.",
|
||||
|
@ -1732,7 +1732,14 @@
|
|||
"EnableAudioNormalization": "Äänen normalisointi",
|
||||
"LabelEnableLUFSScan": "Suorita LUFS-tarkistus",
|
||||
"LabelEnableLUFSScanHelp": "Käytä musiikin LUFS-tarkistusta (tämä vaatii enemmän aikaa ja resurseja).",
|
||||
"GetThePlugin": "Hanki laajennus",
|
||||
"GetThePlugin": "Hanki lisäosa",
|
||||
"Notifications": "Ilmoitukset",
|
||||
"NotificationsMovedMessage": "Ilmoitustoiminnallisuus on siirtynyt Webhook-laajennukseen."
|
||||
"NotificationsMovedMessage": "Ilmoitustoiminnallisuus on siirtynyt Webhook-lisäosaan.",
|
||||
"PasswordRequiredForAdmin": "Ylläpitotileille on määritettävä salasana.",
|
||||
"LabelSyncPlayNoGroups": "Ryhmiä ei ole käytettävissä",
|
||||
"HeaderConfirmRepositoryInstallation": "Vahvista lisäosahakemiston asennus",
|
||||
"LabelDeveloper": "Kehittäjä",
|
||||
"MessageRepositoryInstallDisclaimer": "VAROITUS: Ulkopuolisten tahojen kehittämien lisäosien asennus on aina riskialtista ja ne voivat sisältää epävakaata tai haitallista koodia, ja muuttua koska tahansa. Asenna lisäosia vain kehittäjiltä, joihin luotat.",
|
||||
"Unknown": "Tuntematon",
|
||||
"PleaseConfirmRepositoryInstallation": "Vahvista lukeneesi yllä olevan tekstin ja jatkaaksesi lisäosan asennusta painamalla OK."
|
||||
}
|
||||
|
|
|
@ -804,7 +804,7 @@
|
|||
"MessagePleaseEnsureInternetMetadata": "Veuillez vous assurer que le téléchargement des métadonnées depuis Internet est activé.",
|
||||
"MessagePleaseWait": "Veuillez patienter. Ceci peut prendre quelques minutes.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Pour configurer cette extension, veuillez vous connecter directement à votre serveur local.",
|
||||
"MessagePluginInstallDisclaimer": "Les extensions développées par les membres de la communauté sont une excellente manière d'améliorer votre expérience avec de nouvelles fonctionnalités. Avant toute installation, veuillez prendre connaissance de l'impact qu'elles peuvent avoir sur le serveur, comme l'augmentation de la durée d'actualisation de la médiathèque, de nouvelles tâches de fond, ou un système moins stable.",
|
||||
"MessagePluginInstallDisclaimer": "ATTENTION : Installer une extension tierce comporte des risques. Celle-ci peut contenir du code instable ou malveillant et peut être modifiée à tout moment. N'installez que des extensions provenant d'auteurs en qui vous avez confiance et soyez conscient des effets potentiels que cela peut avoir, requête vers des services externes, augmentation de la durée d'actualisation de la médiathèque, ou tâches de fonds additionnelles.",
|
||||
"MessageReenableUser": "Voir ci-dessous pour le réactiver",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "Ces emplacements de médias vont être supprimés de votre médiathèque",
|
||||
"MessageUnableToConnectToServer": "Nous sommes dans l'impossibilité de nous connecter au serveur sélectionné. Veuillez vérifier qu'il est opérationnel et réessayez.",
|
||||
|
@ -1508,7 +1508,7 @@
|
|||
"Framerate": "Images par seconde",
|
||||
"DisablePlugin": "Désactiver",
|
||||
"EnablePlugin": "Activer",
|
||||
"DirectPlayHelp": "Le fichier source est entièrement compatible avec le client et la session reçoit le fichier sans modifications.",
|
||||
"DirectPlayHelp": "Le fichier source est entièrement compatible avec ce client et la session reçoit le fichier sans modifications.",
|
||||
"HeaderContinueReading": "Reprendre la lecture",
|
||||
"TextSent": "Message envoyé.",
|
||||
"EnableGamepadHelp": "Détecter le signal d'entrée de toute manette connectée. (Nécessite le mode d’affichage 'TV'.)",
|
||||
|
@ -1697,7 +1697,7 @@
|
|||
"LabelDummyChapterDurationHelp": "Intervalle entre deux chapitres factices ou 0 pour désactiver la génération. Changer la valeur n’affectera pas les chapitres existants.",
|
||||
"LabelDummyChapterCount": "Limite",
|
||||
"LabelChapterImageResolution": "Résolution",
|
||||
"LabelChapterImageResolutionHelp": "La résolution d'image des chapitre factices. Changer la valeur n’aura aucun effet sur les chapitres existants.",
|
||||
"LabelChapterImageResolutionHelp": "La résolution d'image des chapitre factices. Changer la valeur n’affectera pas les chapitres existants.",
|
||||
"ResolutionMatchSource": "Résolution de la source",
|
||||
"HeaderDummyChapter": "Images des chapitres",
|
||||
"LabelDummyChapterDuration": "Intervalle",
|
||||
|
@ -1734,8 +1734,14 @@
|
|||
"EnableAudioNormalization": "Normalisation audio",
|
||||
"LabelEnableLUFSScan": "Activer l’analyse LUFS",
|
||||
"LabelEnableLUFSScanHelp": "Activer l’analyse LUFS pour la musique (cela prendra plus de temps et de ressources).",
|
||||
"GetThePlugin": "Obtenir le plugin",
|
||||
"GetThePlugin": "Obtenir l'extension",
|
||||
"Notifications": "Notifications",
|
||||
"NotificationsMovedMessage": "La fonctionnalité de notifications a été transférée au plugin Webhook.",
|
||||
"PasswordRequiredForAdmin": "Un mot de passe est requis pour les comptes administrateur."
|
||||
"NotificationsMovedMessage": "La fonctionnalité de notifications a été transférée à l'extension Webhook.",
|
||||
"PasswordRequiredForAdmin": "Un mot de passe est requis pour les comptes administrateur.",
|
||||
"LabelSyncPlayNoGroups": "Pas de groupes disponibles",
|
||||
"HeaderConfirmRepositoryInstallation": "Confirmer l'installation du dépôt d'extensions",
|
||||
"LabelDeveloper": "Développeur",
|
||||
"Unknown": "Inconnu",
|
||||
"MessageRepositoryInstallDisclaimer": "ATTENTION : Installer un dépôt d'extensions tierces comporte des risques. Celui-ci peut contenir du code instable ou malveillant et peut être modifié à tout moment. N'installez que des dépôts provenant d'auteurs en qui vous avez confiance.",
|
||||
"PleaseConfirmRepositoryInstallation": "Veuillez cliquer sur OK pour confirmer que vous avez lu ce qui précède et que vous souhaitez poursuivre l'installation du dépôt d'extensions."
|
||||
}
|
||||
|
|
|
@ -1056,5 +1056,6 @@
|
|||
"HeaderPerformance": "ביצועים",
|
||||
"HeaderRecordingMetadataSaving": "מטאדאטה של הקלטות",
|
||||
"HeaderDummyChapter": "תמונות פרק",
|
||||
"Unreleased": "לא יצא עדיין"
|
||||
"Unreleased": "לא יצא עדיין",
|
||||
"AllowCollectionManagement": "הרשה למשתמש זה לנהל אוספים"
|
||||
}
|
||||
|
|
|
@ -1361,5 +1361,14 @@
|
|||
"LabelAutomaticallyAddToCollectionHelp": "Kada najmanje 2 filma imaju isti naziv kolekcije, automatski će se dodati u kolekciju.",
|
||||
"Suggestions": "Prijedlozi",
|
||||
"Trailers": "Najave",
|
||||
"Small": "Malo"
|
||||
"Small": "Malo",
|
||||
"HeaderDummyChapter": "Slike Poglavlja",
|
||||
"EnableAudioNormalization": "Normalizacija zvuka",
|
||||
"Experimental": "Eksperimentalno",
|
||||
"HeaderPerformance": "Performanse",
|
||||
"DownloadAll": "Preuzmi sve",
|
||||
"HeaderRecordingMetadataSaving": "Snimanje metapodataka",
|
||||
"LabelChapterImageResolution": "Rezolucija",
|
||||
"AllowCollectionManagement": "Dozvoli ovom korisniku da upravlja kolekcijama",
|
||||
"LabelDummyChapterDuration": "Interval"
|
||||
}
|
||||
|
|
|
@ -388,7 +388,7 @@
|
|||
"Subtitles": "Feliratok",
|
||||
"Suggestions": "Javaslatok",
|
||||
"Sunday": "Vasárnap",
|
||||
"Sync": "Szinkronizál",
|
||||
"Sync": "Szinkronizálás",
|
||||
"TabAccess": "Hozzáférés",
|
||||
"TabAdvanced": "Speciális",
|
||||
"TabCatalog": "Katalógus",
|
||||
|
@ -563,7 +563,7 @@
|
|||
"HeaderActivity": "Tevékenységek",
|
||||
"HeaderAdditionalParts": "További részek",
|
||||
"HeaderAdmin": "Felügyelet",
|
||||
"HeaderAlbumArtists": "Album előadó(k)",
|
||||
"HeaderAlbumArtists": "Albumelőadók",
|
||||
"HeaderAlert": "Figyelem",
|
||||
"HeaderAllowMediaDeletionFrom": "Médiatörlés engedélyezése innen",
|
||||
"HeaderApiKey": "API kulcs",
|
||||
|
@ -638,8 +638,8 @@
|
|||
"Photos": "Fényképek",
|
||||
"Playlists": "Lejátszási listák",
|
||||
"Shows": "Sorozatok",
|
||||
"Songs": "Dalok",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"Songs": "Számok",
|
||||
"ValueSpecialEpisodeName": "Különkiadás – {0}",
|
||||
"EnableThemeVideosHelp": "Témavideókat játszhat a háttérben a könyvtár böngészése közben.",
|
||||
"HeaderBlockItemsWithNoRating": "Blokkolja azokat az elemeket amelyek tiltott, vagy nem felismerhető minősítésűek",
|
||||
"HeaderSeriesStatus": "Sorozat állapot",
|
||||
|
|
|
@ -768,7 +768,7 @@
|
|||
"MessagePleaseEnsureInternetMetadata": "Assicurarsi che il download dei metadati Internet sia abilitato.",
|
||||
"MessagePleaseWait": "Per favore attendi. La procedura potrebbe impiegare qualche minuto.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Per configurare questo plugin si prega di accedere al proprio server locale direttamente.",
|
||||
"MessagePluginInstallDisclaimer": "I plugin creati dai membri della comunità sono un ottimo modo per migliorare l'esperienza con funzionalità e vantaggi aggiuntivi. Prima di installarli, si prega di notare gli effetti che possono avere sul tuo Server, come le scansioni più lunghe della libreria, l'elaborazione di sfondo aggiuntiva e la stabilità del sistema diminuita.",
|
||||
"MessagePluginInstallDisclaimer": "ATTENZIONE: L'installazione di plugin di terze parti può portare dei rischi. Può contenere codice instabile o malevolo e può cambiare in qualsiasi momento. Installa solo plugin degli autori di cui ti fidi e tieni presente gli effetti collaterali che potrebbe avere, inclusi query esterne, scan più lunghi or processi in background aggiuntivi.",
|
||||
"MessageReenableUser": "Guarda in basso per ri-abilitare",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "I seguenti percorsi ai file multimediali saranno rimossi dalla tua libreria",
|
||||
"MessageUnableToConnectToServer": "Non siamo in grado di connettersi al server selezionato al momento. Per favore assicurati che sia in esecuzione e riprova.",
|
||||
|
@ -1731,5 +1731,17 @@
|
|||
"Studio": "Studio",
|
||||
"GetThePlugin": "Ottieni il Plugin",
|
||||
"Notifications": "Notifiche",
|
||||
"AllowCollectionManagement": "Permetti a questo utente di gestire le collezioni"
|
||||
"AllowCollectionManagement": "Permetti a questo utente di gestire le collezioni",
|
||||
"PasswordRequiredForAdmin": "La password è obbligatoria per gli account amministratori.",
|
||||
"LabelSyncPlayNoGroups": "Nessun gruppo disponibile",
|
||||
"NotificationsMovedMessage": "Le notifiche sono state spostate nel plugin Webhook.",
|
||||
"EnableAudioNormalizationHelp": "La normalizzazione dell'audio aggiunge un guadagno per mantenerlo ad un livello desiderato (-18dB).",
|
||||
"EnableAudioNormalization": "Normalizzazione Audio",
|
||||
"PleaseConfirmRepositoryInstallation": "Clicca OK per confermare di aver letto sopra e desideri procedere con l'installazione del repository del plugin.",
|
||||
"HeaderConfirmRepositoryInstallation": "Conferma dell'installazione del repository dei plugin",
|
||||
"LabelDeveloper": "Sviluppatore",
|
||||
"LabelEnableLUFSScan": "Abilita LUFS scan",
|
||||
"LabelEnableLUFSScanHelp": "Abilita LUFS scan per la musica (Impiegherà più tempo e più risorse).",
|
||||
"MessageRepositoryInstallDisclaimer": "ATTENZIONE: L'installazione di repository di plugin di terze parti può portare dei rischi. Può contenere codice instabile o malevolo e può cambiare in qualsiasi momento. Installa solo plugin degli autori di cui ti fidi.",
|
||||
"Unknown": "Sconosciuto"
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
"AskAdminToCreateLibrary": "Minta pentadbir untuk membuat perpustakaan.",
|
||||
"Artist": "Artis",
|
||||
"ApiKeysCaption": "Senarai kunci API yang diaktifkan sekarang",
|
||||
"AllowTonemappingHelp": "Pemetaan nada dapat mengubah rentang dinamik video dari HDR ke SDR sambil mengekalkan perincian dan warna gambar, yang merupakan maklumat yang sangat penting untuk mewakili pemandangan asal. Pada masa ini ia hanya berfungsi dengan video HDR10 atau HLG. Ini memerlukan pemadanan dengan OpenCL atau CUDA semasa.",
|
||||
"AllowTonemappingHelp": "Pemetaan-tone dapat mengubah dinamik video daripada HDR ke SDR sambil mengekalkan perincian dan warna gambar, yang merupakan maklumat yang sangat penting untuk mewakili pemandangan asal. Pada masa ini ia hanya berfungsi dengan video 10bit HDR10, HLG dan Dolby Visoion. Ini memerlukan pemadanan bersesuian dengan OpenCL atau CUDA runtime.",
|
||||
"Songs": "Lagu-lagu",
|
||||
"Playlists": "Senarai ulangmain",
|
||||
"Photos": "Gambar-gambar",
|
||||
|
@ -240,5 +240,11 @@
|
|||
"ButtonExitApp": "Tamatkan aplikasi",
|
||||
"ButtonClose": "Tutup",
|
||||
"AgeValue": "({0} tahun)",
|
||||
"AddToFavorites": "Tambah ke kegemaran"
|
||||
"AddToFavorites": "Tambah ke kegemaran",
|
||||
"Arranger": "Penyusunan",
|
||||
"DefaultMetadataLangaugeDescription": "Ini adalah tetapan umum dan boleh diubahsuai mengikut koleksi.",
|
||||
"EnableAudioNormalizationHelp": "Normilasi audio akan meletakkan gain yang konstant agar purata di tahap yang dikehendaki (-18dB).",
|
||||
"Console": "konsol",
|
||||
"DeathDateValue": "Died: {0} , program error",
|
||||
"AllowCollectionManagement": "Benarkan pengguna ini meguruskan koleksi"
|
||||
}
|
||||
|
|
22
src/strings/mt.json
Normal file
22
src/strings/mt.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"Absolute": "Assolut",
|
||||
"AddToPlaylist": "Żid fil-playlist",
|
||||
"AddToPlayQueue": "Żid fil-play queue",
|
||||
"Album": "Album",
|
||||
"AllEpisodes": "L-episodji kollha",
|
||||
"AllLanguages": "Il-lingwi kollha",
|
||||
"AllLibraries": "Il-libreriji kollha",
|
||||
"AccessRestrictedTryAgainLater": "Bħalissa l-aċċess huwa ristrett. Jekk jogħgbok erġa' prova iktar tard.",
|
||||
"Actor": "Attur",
|
||||
"Add": "Żid",
|
||||
"AddedOnValue": "{0} Miżjuda",
|
||||
"AddToCollection": "Żid fil-kollezzjoni",
|
||||
"AddToFavorites": "Żid fil-lista tal-favoriti",
|
||||
"AgeValue": "({0} snin)",
|
||||
"AirDate": "Data tax-xandir",
|
||||
"Aired": "Imxandar",
|
||||
"Albums": "Albums",
|
||||
"All": "Kollox",
|
||||
"AllChannels": "L-istazzjonijiet kollha",
|
||||
"AllComplexFormats": "Il-formats ikkumplikati kollha (ASS, SSA, VobSub, PGS, SUB, IDX, …)"
|
||||
}
|
|
@ -1418,7 +1418,7 @@
|
|||
"LabelTonemappingRange": "Tonekartlegging-område",
|
||||
"TonemappingAlgorithmHelp": "Tonekartlegging kan finjusteres. Hvis du ikke er kjent med disse alternativene er det bare å beholde standardinnstillingen. Anbefalt verdi er BT.2390.",
|
||||
"LabelTonemappingAlgorithm": "Velg algoritmen som skal brukes for tonekartlegging",
|
||||
"AllowTonemappingHelp": "Tonekartlegging kan forvandle det dynamiske området på en video fra HDR til SDR samtidig som bildedetaljer og farger opprettholdes, noe som er veldig viktig informasjon for å representere den opprinnelige scenen. Fungerer for øyeblikket bare med HDR10 eller HLG videoer. Dette krever korresponderende OpenCL eller CUDA runtime.",
|
||||
"AllowTonemappingHelp": "Tonekartlegging kan forvandle det dynamiske området på en video fra HDR til SDR samtidig som bildedetaljer og farger opprettholdes, noe som er veldig viktig informasjon for å representere den opprinnelige scenen. Fungerer for øyeblikket bare med HDR10-, HLG- og DoVi-videoer. Dette krever korresponderende OpenCL eller CUDA runtime.",
|
||||
"OptionMaxActiveSessionsHelp": "En verdi på 0 skrur av denne funksjonen.",
|
||||
"OptionMaxActiveSessions": "Sett maksimalt antall tilgjengelige brukerøkter.",
|
||||
"LabelUserMaxActiveSessions": "Maksimalt antall samtidige brukerøkter",
|
||||
|
@ -1663,7 +1663,7 @@
|
|||
"IgnoreDtsHelp": "Deaktiviering av dette valget kan løse noen problemer, f.eks. manglende lyd på kanaler med seperat lyd og videospor.",
|
||||
"IgnoreDts": "Ignorer DTS (dekoding tidsmerke)",
|
||||
"MessageNoFavoritesAvailable": "Ingen favoritter er tilgjengelige for øyeblikket.",
|
||||
"LabelVppTonemappingBrightnessHelp": "Bruk lysstyrkeforsterkning i VPP-tonemapping. Både anbefalte og standardverdier er 0.",
|
||||
"LabelVppTonemappingBrightnessHelp": "Bruk lysstyrkeforsterkning i VPP-tonemapping. Både anbefalte og standardverdier er 16 og 0.",
|
||||
"MediaInfoDoViTitle": "DV tittel",
|
||||
"MediaInfoElPresentFlag": "DV el forhåndsinnstilt flagg",
|
||||
"Unreleased": "Ennå ikke utgitt",
|
||||
|
@ -1672,7 +1672,7 @@
|
|||
"MediaInfoDvLevel": "DV nivå",
|
||||
"MediaInfoRpuPresentFlag": "DV rpu forhåndsinnstilt flagg",
|
||||
"LabelVppTonemappingContrast": "VPP-tonemapping kontrastforsterkning",
|
||||
"LabelVppTonemappingContrastHelp": "Bruk kontrastforsterkning i VPP-tonemapping. De anbefalte og standardverdiene er 1.2 og 1.",
|
||||
"LabelVppTonemappingContrastHelp": "Bruk kontrastforsterkning i VPP-tonemapping. Den anbefalte standardverdien er 1.",
|
||||
"VideoRangeTypeNotSupported": "Videoens områdetype støttes ikke",
|
||||
"LabelVideoRangeType": "Video områdetype",
|
||||
"MediaInfoVideoRangeType": "Video områdetype",
|
||||
|
@ -1694,7 +1694,7 @@
|
|||
"LabelDummyChapterDuration": "Intervall",
|
||||
"HeaderRecordingMetadataSaving": "Opptak metadata",
|
||||
"HeaderPerformance": "Ytelse",
|
||||
"LabelDummyChapterDurationHelp": "Tid mellom innsamling av kapittelbilder i sekunder.",
|
||||
"LabelDummyChapterDurationHelp": "Tid mellom dummykapitler. Sett til 0 for å slå av dummykapittelgenerering. Endring av dette vil ha ingen effekt på eksiterende dummykapitler.",
|
||||
"LabelEnableAudioVbrHelp": "Variabel bithastighet tilbyr bedre forhold mellom kvalitet og gjennomsnittlig bithastighet, men kan i visse tilfeller forårsake buffering og problemer med kompatibilitet.",
|
||||
"LabelEnableAudioVbr": "Aktiver VBR lydkoding",
|
||||
"SubtitleBlack": "Svart",
|
||||
|
@ -1710,5 +1710,32 @@
|
|||
"SubtitleRed": "Rød",
|
||||
"SubtitleYellow": "Gul",
|
||||
"SubtitleWhite": "Hvit",
|
||||
"Select": "Velg"
|
||||
"Select": "Velg",
|
||||
"SaveRecordingImages": "Lagre EPG-opptaksbilder",
|
||||
"PasswordRequiredForAdmin": "Et passord et påkrevd for administratorkontoer.",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNames": "Foretrekk innbakte titler foran filnavn for ekstramateriell",
|
||||
"SaveRecordingNFO": "Lagre EPG-opptaksdata i NFO",
|
||||
"SaveRecordingImagesHelp": "Lagre bilder fra EPG-listekilde sammen med media.",
|
||||
"StereoDownmixAlgorithmHelp": "Algoritme benyttet til å mikse ned multikanals lyd til stereo.",
|
||||
"Studio": "Studio",
|
||||
"SubtitleCyan": "Turkis",
|
||||
"UserMenu": "Brukermenyen",
|
||||
"Featurette": "Featurette",
|
||||
"LabelTonemappingMode": "Tonemappingsmodus",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Ekstramateriale har ofte det samme innebygde navnet som det opprinnelige materialet. Kryss av for denne for å bruke den innebygde tittelen likevel.",
|
||||
"LabelSyncPlayNoGroups": "Ingen grupper tilgjengelig",
|
||||
"NotificationsMovedMessage": "Varslingsfunksjonaliteten er blitt flyttet til Webhook-programtillegget.",
|
||||
"EnableAudioNormalization": "Lynormalisering",
|
||||
"GetThePlugin": "Skaff deg programtillegget",
|
||||
"Notifications": "Varsler",
|
||||
"TonemappingModeHelp": "Velg tonemappingsmodus. Hvis du merker utvaskede høylysområder, prøv å bytte til RGB-modus.",
|
||||
"EnableAudioNormalizationHelp": "Lydnormalisering vil legge på en konstant lydforsterkning for å holde gjennomsnittet på ønsket nivå (-18db).",
|
||||
"LabelEnableLUFSScan": "Slå på LUFS-skanning",
|
||||
"LabelParallelImageEncodingLimitHelp": "Høyeste antall bildeenkodinger som tillates å kjøre parallelt. Å sette denne til 0 vil velge en grensse basert på systemspesifikasjonene dine.",
|
||||
"LabelChapterImageResolutionHelp": "Oppløsningen til kapittelbildene. Enring av dette vil ikke ha noen effekt på eksisterende kapittelbilder.",
|
||||
"LabelParallelImageEncodingLimit": "Parallell bildeenkodingsgrense",
|
||||
"LabelEnableLUFSScanHelp": "Slå på LUFS-skanning for musikk (dette vil ta lengre tid og mere ressurser).",
|
||||
"ResolutionMatchSource": "Bruk kildetreff",
|
||||
"SaveRecordingNFOHelp": "Lagre metadata fra EPG-listekilde sammen med media.",
|
||||
"AllowCollectionManagement": "La denne brukeren organisere samlinger"
|
||||
}
|
||||
|
|
|
@ -759,7 +759,7 @@
|
|||
"MessagePleaseEnsureInternetMetadata": "Zorg ervoor dat het downloaden van internet-metadata is ingeschakeld.",
|
||||
"MessagePleaseWait": "Even geduld. Dit kan even duren.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Om deze plug-in te configuren moet je je direct op de lokale server aanmelden.",
|
||||
"MessagePluginInstallDisclaimer": "Plug-ins ontwikkeld door leden van de gemeenschap zijn een geweldige manier om uw ervaring met extra functies en voordelen te verbeteren. Wees voor het installeren bewust van de effecten die zij op uw server kunnen hebben, zoals langere bibliotheekscans, meer achtergrondverwerking en een verminderde stabiliteit van het systeem.",
|
||||
"MessagePluginInstallDisclaimer": "WAARSCHUWING: het installeren van een plug-in van derden brengt risico's met zich mee. De plug-in kan ieder moment veranderen en instabiele of kwaadaardige code bevatten. Installeer alleen plug-ins van auteurs die je vertrouwt en wees je bewust van de mogelijke gevolgen, zoals verbindingen met externe diensten, langer durende bibliotheekscans of meer achtergrondverwerking.",
|
||||
"MessageReenableUser": "Zie hieronder hoe opnieuw in te schakelen",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "De volgende medialocaties worden verwijderd uit je bibliotheek",
|
||||
"MessageUnableToConnectToServer": "Het is momenteel niet mogelijk met de geselecteerde server te verbinden. Controleer of deze draait en probeer het opnieuw.",
|
||||
|
@ -1736,5 +1736,11 @@
|
|||
"EnableAudioNormalizationHelp": "Geluidsnormalisatie past een constante versterking toe om het gemiddelde op een gewenst niveau (-18dB) te houden.",
|
||||
"LabelEnableLUFSScan": "LUFS-scan inschakelen",
|
||||
"LabelEnableLUFSScanHelp": "LUFS-scan voor muziek inschakelen. Dit duurt langer en is systeemintensief.",
|
||||
"PasswordRequiredForAdmin": "Voor beheerdersaccounts is een wachtwoord vereist."
|
||||
"PasswordRequiredForAdmin": "Voor beheerdersaccounts is een wachtwoord vereist.",
|
||||
"LabelSyncPlayNoGroups": "Geen groepen beschikbaar",
|
||||
"HeaderConfirmRepositoryInstallation": "Installatie plug-in-repository bevestigen",
|
||||
"LabelDeveloper": "Ontwikkelaar",
|
||||
"MessageRepositoryInstallDisclaimer": "WAARSCHUWING: het installeren van een plug-in-repository van derden brengt risico's met zich mee. De repository kan ieder moment veranderen en instabiele of kwaadaardige code bevatten. Installeer alleen repository's van auteurs die je vertrouwt.",
|
||||
"PleaseConfirmRepositoryInstallation": "Druk op OK als je bovenstaande gelezen hebt en wenst door te gaan met de installatie van de plug-in-repository.",
|
||||
"Unknown": "Onbekend"
|
||||
}
|
||||
|
|
|
@ -1693,10 +1693,10 @@
|
|||
"SaveRecordingNFOHelp": "Zapisz metadane od dostawcy list EPG wraz z mediami.",
|
||||
"HeaderDummyChapter": "Obrazy rozdziału",
|
||||
"LabelDummyChapterDuration": "Interwał",
|
||||
"LabelDummyChapterDurationHelp": "Interwał ekstrakcji obrazu rozdziału w sekundach.",
|
||||
"LabelDummyChapterDurationHelp": "Odstęp między fikcyjnymi rozdziałami. Ustaw na 0, aby wyłączyć generowanie fikcyjnych rozdziałów. Zmiana tego nie będzie miała wpływu na istniejące fikcyjne rozdziały.",
|
||||
"LabelDummyChapterCount": "Ograniczenie",
|
||||
"LabelChapterImageResolution": "Rozdzielczość",
|
||||
"LabelChapterImageResolutionHelp": "Rozdzielczość wyodrębnionych obrazów rozdziałów.",
|
||||
"LabelChapterImageResolutionHelp": "Rozdzielczość wyodrębnionych obrazów rozdziałów. Zmiana tego ustawienia nie ma wpływu na istniejące rozdziały.",
|
||||
"ResolutionMatchSource": "Źródło dopasowania",
|
||||
"SaveRecordingNFO": "Zapisz nagrane metadane EPG w NFO",
|
||||
"SaveRecordingImages": "Zapisywanie obrazów EPG",
|
||||
|
@ -1734,5 +1734,14 @@
|
|||
"EnableAudioNormalization": "Normalizacja dźwięku",
|
||||
"LabelEnableLUFSScan": "Włącz skanowanie głośności dźwięku",
|
||||
"LabelEnableLUFSScanHelp": "Włącz skanowanie głośności dźwięku dla muzyki (To zajmie więcej czasu i więcej zasobów).",
|
||||
"Notifications": "Powiadomienia"
|
||||
"Notifications": "Powiadomienia",
|
||||
"PasswordRequiredForAdmin": "Hasło jest wymagane do kont administratorów.",
|
||||
"LabelSyncPlayNoGroups": "Brak dostępnych grup",
|
||||
"GetThePlugin": "Pobierz wtyczkę",
|
||||
"NotificationsMovedMessage": "Funkcjonalność powiadomień została przeniesiona do wtyczki Webhook.",
|
||||
"HeaderConfirmRepositoryInstallation": "Potwierdź instalację repozytorium wtyczek",
|
||||
"LabelDeveloper": "Programista",
|
||||
"MessageRepositoryInstallDisclaimer": "OSTRZEŻENIE: Instalacja repozytorium wtyczek stron trzecich wiąże się z ryzykiem. Może ono zawierać niestabilny lub złośliwy kod i może ulec zmianie w dowolnym momencie. Instaluj tylko repozytoria od zaufanych autorów.",
|
||||
"PleaseConfirmRepositoryInstallation": "Kliknij OK, aby potwierdzić, że przeczytałeś powyższe informacje i chcesz kontynuować instalację repozytorium wtyczek.",
|
||||
"Unknown": "Nieznane"
|
||||
}
|
||||
|
|
|
@ -1182,7 +1182,7 @@
|
|||
"OptionLoginAttemptsBeforeLockoutHelp": "Um valor zero significa herdar o padrão de três tentativas para usuários normais e cinco para administradores. Definir como -1 desativará o recurso.",
|
||||
"OptionProtocolHls": "Transmissão ao Vivo por HTTP (HLS)",
|
||||
"OptionProtocolHttp": "HTTP",
|
||||
"OptionRegex": "Regex",
|
||||
"OptionRegex": "Expressão Regular",
|
||||
"OptionSubstring": "Substring",
|
||||
"PasswordResetProviderHelp": "Escolha um provedor de reset de senha a ser usado quando este usuário solicitar uma redefinição de senha.",
|
||||
"PictureInPicture": "vídeo destacado",
|
||||
|
@ -1564,7 +1564,7 @@
|
|||
"Remixer": "Remixar",
|
||||
"ReleaseGroup": "Grupo de Realease",
|
||||
"PlaybackErrorPlaceHolder": "Este é um espaço reservado para mídia física que o Jellyfin não pode reproduzir. Insira o disco a ser reproduzido.",
|
||||
"Mixer": "Mixer",
|
||||
"Mixer": "Mixador",
|
||||
"LabelSyncPlaySettingsSkipToSyncHelp": "Método de correção de sincronismo que consiste em buscar a posição estimada. A correção de sincronização deve estar habilitada.",
|
||||
"LabelSyncPlaySettingsSkipToSync": "Habilitar SkipToSync",
|
||||
"LabelSyncPlaySettingsSpeedToSyncHelp": "Método de correção de sincronização que consiste em acelerar a reprodução. A correção de sincronização deve estar habilitada.",
|
||||
|
|
|
@ -607,7 +607,7 @@
|
|||
"XmlDocumentAttributeListHelp": "Estes atributos são aplicados ao elemento principal de cada resposta XML.",
|
||||
"AccessRestrictedTryAgainLater": "O acesso está atualmente restrito. Por favor, tente mais tarde.",
|
||||
"AddToCollection": "Adicionar à coleção",
|
||||
"AddToPlayQueue": "Adicionar à fila de reprodução",
|
||||
"AddToPlayQueue": "Adicionar à lista de reprodução",
|
||||
"AddedOnValue": "Adicionado {0}",
|
||||
"AirDate": "Data de estreia",
|
||||
"Albums": "Álbuns",
|
||||
|
@ -1525,7 +1525,7 @@
|
|||
"DirectPlayHelp": "O ficheiro de origem é totalmente compatível com este cliente e a sessão está a receber o ficheiro sem modificações.",
|
||||
"Conductor": "Maestro",
|
||||
"Arranger": "Organizador",
|
||||
"AgeValue": "({0} anos de idade)",
|
||||
"AgeValue": "({0} anos)",
|
||||
"LabelSyncPlaySettingsSpeedToSync": "Ativar SpeedToSync",
|
||||
"LabelAutomaticallyAddToCollection": "Adicionar automaticamente à colecção",
|
||||
"ItemDetails": "Detalhes",
|
||||
|
@ -1677,5 +1677,53 @@
|
|||
"BehindTheScenes": "Nos bastidores",
|
||||
"DownloadAll": "Transferir Todas",
|
||||
"MessageNoItemsAvailable": "Nenhum item disponível atualmente.",
|
||||
"MessageNoFavoritesAvailable": "Nenhum favorito disponível atualmente."
|
||||
"MessageNoFavoritesAvailable": "Nenhum favorito disponível atualmente.",
|
||||
"SubtitleBlack": "Preto",
|
||||
"Short": "Curta-metragem",
|
||||
"PasswordRequiredForAdmin": "A password é obrigatória para contas de administrador.",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNames": "Prefira títulos incorporados em vez de nomes de ficheiros para extras",
|
||||
"SubtitleGreen": "Verde",
|
||||
"SubtitleLightGray": "Cinzento Claro",
|
||||
"SubtitleMagenta": "Magenta",
|
||||
"SubtitleRed": "Vermelho",
|
||||
"SubtitleYellow": "Amarelo",
|
||||
"Featurette": "Featurette",
|
||||
"Studio": "Estúdio",
|
||||
"StereoDownmixAlgorithmHelp": "Algoritmo usado para fazer downmix de áudio multicanal para estéreo.",
|
||||
"Unreleased": "Ainda não foi lançado",
|
||||
"SubtitleBlue": "Azul",
|
||||
"SubtitleCyan": "Ciano",
|
||||
"LabelSyncPlayNoGroups": "Não há grupos disponíveis",
|
||||
"Select": "Selecciona",
|
||||
"AllowCollectionManagement": "Permitir este utilizador gerir as coleções",
|
||||
"SecondarySubtitles": "Legendas secundárias",
|
||||
"LabelDummyChapterDuration": "Intervalo",
|
||||
"LabelDummyChapterDurationHelp": "O intervalo entre os capítulos fictícios. Defina como 0 para desativar a criação de capítulo fictício. Alterar isso não terá efeito nos capítulos fictícios existentes.",
|
||||
"LabelChapterImageResolution": "Resolução",
|
||||
"LabelChapterImageResolutionHelp": "A resolução das imagens do capítulo extraídas. Alterar isso não terá efeito nos capítulos fictícios existentes.",
|
||||
"LabelParallelImageEncodingLimit": "Limite de codificação paralela de imagens",
|
||||
"LabelParallelImageEncodingLimitHelp": "Quantidade máxima de codificações de imagem permitidas para execução em paralelo. Definir como 0 escolherá um limite com base nas especificações do sistema.",
|
||||
"GetThePlugin": "Obter Plugin",
|
||||
"Notifications": "Notificações",
|
||||
"NotificationsMovedMessage": "A funcionalidade de notificações foi movida para o plugin Webhook.",
|
||||
"LabelEnableAudioVbr": "Ativar codificação do áudio VBR",
|
||||
"ResolutionMatchSource": "Correspondente ao original",
|
||||
"MenuOpen": "Abrir menu",
|
||||
"MenuClose": "Fechar menu",
|
||||
"SubtitleGray": "Cinzento",
|
||||
"SubtitleWhite": "Branco",
|
||||
"UserMenu": "Menu de utilizador",
|
||||
"HeaderPerformance": "Desempenho",
|
||||
"EnableAudioNormalizationHelp": "A normalização do áudio adicionará um ganho constante para manter a média no nível desejado (-18dB).",
|
||||
"EnableAudioNormalization": "Normalização de áudio",
|
||||
"HeaderRecordingMetadataSaving": "Gravar metadados",
|
||||
"OptionDateEpisodeAdded": "Data de adição do episódio",
|
||||
"LabelEnableLUFSScanHelp": "Ative a verificação LUFS para música (isso levará mais tempo e mais recursos).",
|
||||
"LabelEnableLUFSScan": "Ative a verificação LUFS",
|
||||
"HeaderDummyChapter": "Imagens dos Capítulos",
|
||||
"LabelEnableAudioVbrHelp": "A taxa de bits variável oferece melhor qualidade relativamente à taxa de bits média, mas, em alguns casos raros, pode causar problemas de ‘buffer’ e compatibilidade.",
|
||||
"Experimental": "Experimental",
|
||||
"MessageRenameMediaFolder": "Renomear uma biblioteca de multimédia fará com que todos os metadados sejam perdidos, proceda com cautela.",
|
||||
"EnableCardLayout": "Mostrar em formato mosaico",
|
||||
"OptionDateShowAdded": "Data de adição da Série"
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"AllowRemoteAccessHelp": "Nezaškrtnuté znamená, že všetky vzdialené pripojenia budú blokované.",
|
||||
"AlwaysPlaySubtitles": "Vždy zobrazovať",
|
||||
"AnyLanguage": "Akýkoľvek jazyk",
|
||||
"AroundTime": "Okolo",
|
||||
"AroundTime": "Okolo {0}",
|
||||
"Artists": "Interpreti",
|
||||
"AsManyAsPossible": "Najviac ako je možné",
|
||||
"Ascending": "Vzostupne",
|
||||
|
@ -1447,7 +1447,7 @@
|
|||
"AspectRatioCover": "Obal",
|
||||
"VideoAudio": "Video Zvuk",
|
||||
"Video": "Video",
|
||||
"AllowTonemappingHelp": "Mapovanie tónov umožňuje zmeniť dynamicky rozsah videa z HDR na SDR bez straty veľmi dôležitých informácií o pôvodnom obraze, ako napr. detaily a farby. V súčasnosti táto funkcia funguje len pri videách s HDR10 alebo HLG. Táto funkcia vyžaduje OpenCL alebo CUDA.",
|
||||
"AllowTonemappingHelp": "Tone-mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colors, which are very important information for representing the original scene. Currently works only with 10bit HDR10, HLG and DoVi videos. This requires the corresponding OpenCL or CUDA runtime.",
|
||||
"LabelTonemappingThresholdHelp": "Parametre algoritmu mapovania tónov sú prispôsobené jednotlivým scénam. A tento prah sa používa na zistenie, či sa scéna zmenila alebo nie. Pokiaľ rozdiel medzi súčasnou priemernou svetlosťou snímku a priebežným priemerom tento prah prekročí, bude priemerná a vrchná svetlosť scény prepočítaná. Doporučené a predvolené hodnoty sú 0.8 a 0.2.",
|
||||
"LabelUDPPortRangeHelp": "Obmedzí UDP pripojenie Jellyfinu na tento rozsah. (Predvolená hodnota je 1024 - 65535).<br/> Poznámka: Niektoré funkcie vyžadujú určité porty, ktoré sa môžu nachádzať mimo tohto rozsahu.",
|
||||
"Remuxing": "Remuxovanie",
|
||||
|
@ -1666,9 +1666,9 @@
|
|||
"MediaInfoVideoRangeType": "Typ rozsahu videa",
|
||||
"LabelVideoRangeType": "Typ rozsahu videa",
|
||||
"VideoRangeTypeNotSupported": "Typ rozsahu videa nie je podporovaný",
|
||||
"LabelVppTonemappingContrastHelp": "Aplikuje zvýšenie kontrastu pri mapovaní VPP tónov. Odporúčaná hodnota je 1.2 a predvolená hodnota je 1.",
|
||||
"LabelVppTonemappingContrastHelp": "Aplikuje zvýšenie kontrastu pri mapovaní VPP tónov. Odporúčaná a predvolená hodnota je 1.",
|
||||
"LabelVppTonemappingContrast": "Zvýšenie kontrastu mapovania VPP tónov",
|
||||
"LabelVppTonemappingBrightnessHelp": "Aplikuje zvýšenie jasu pri mapovaní VPP tónov. Odporúčaná a predvolená hodnota je 0.",
|
||||
"LabelVppTonemappingBrightnessHelp": "Aplikuje zvýšenie jasu pri mapovaní VPP tónov. Odporúčané a predvolené hodnoty sú 16 a 0.",
|
||||
"LabelVppTonemappingBrightness": "Zvýšenie jasu mapovania VPP tónov",
|
||||
"ScreenResolution": "Rozlíšenie obrazovky",
|
||||
"RememberSubtitleSelectionsHelp": "Pokúsi sa nastaviť titulky tak, aby sa čo najviac zhodovali s posledným videom.",
|
||||
|
@ -1694,15 +1694,47 @@
|
|||
"LabelDummyChapterCount": "Limit",
|
||||
"LabelDummyChapterCountHelp": "Maximálny počet obrázkov kapitol, ktoré budú extrahované pre každý mediálny súbor.",
|
||||
"LabelChapterImageResolution": "Rozlíšenie",
|
||||
"LabelChapterImageResolutionHelp": "Rozlíšenie extrahovaných obrázkov kapitol.",
|
||||
"LabelChapterImageResolutionHelp": "Rozlíšenie extrahovaných obrázkov kapitol. Zmena tohto nastavenia nemá žiaden vplyv na existujúce kapitoly.",
|
||||
"ResolutionMatchSource": "Rovnaké ako zdroj",
|
||||
"SaveRecordingNFOHelp": "Uloží metadáta z EPG položiek sprievodcu spolu s médiami.",
|
||||
"SaveRecordingImages": "Uložiť obrázky EPG nahrávky",
|
||||
"LabelDummyChapterDurationHelp": "Interval extrakcie obrázkov kapitol v sekundách.",
|
||||
"LabelDummyChapterDurationHelp": "Interval medzi kapitolami. Vytváranie kapitol je možné vypnúť nastavením na 0. Zmena tohto nastavenia nemá žiaden vplyv na existujúce kapitoly.",
|
||||
"SaveRecordingNFO": "Uložiť metadáta nahrávky zo sprievodcu EPG do NFO",
|
||||
"SaveRecordingImagesHelp": "Uloží obrázky z EPG položiek sprievodcu spolu s médiami.",
|
||||
"HeaderRecordingMetadataSaving": "Metadáta nahrávok",
|
||||
"SecondarySubtitles": "Sekundárne titulky",
|
||||
"LabelEnableAudioVbr": "Povoliť kódovanie zvuku VBR",
|
||||
"HeaderPerformance": "Výkon"
|
||||
"HeaderPerformance": "Výkon",
|
||||
"AllowCollectionManagement": "Povoliť tomuto používateľovi spravovať kolekcie",
|
||||
"LabelParallelImageEncodingLimitHelp": "Maximálny počet kódovania obrázkov, ktoré môžu naraz bežať. Nastavením na 0 bude limit nastavení podľa parametrov systému.",
|
||||
"TonemappingModeHelp": "Vyberte režim mapovania tónu. Ak narazíte na preexponované svetlé miesta, skúste prepnúť na režim RGB.",
|
||||
"Featurette": "Stredne dlhý film",
|
||||
"Short": "Krátky film",
|
||||
"PasswordRequiredForAdmin": "Administrátorské úcty musia mať nastavené heslo.",
|
||||
"LabelTonemappingMode": "Režim mapovania tónu",
|
||||
"SubtitleCyan": "Tyrkysová",
|
||||
"SubtitleGray": "Sivá",
|
||||
"Studio": "Štúdio",
|
||||
"SubtitleBlue": "Modrá",
|
||||
"SubtitleBlack": "Čierna",
|
||||
"SubtitleGreen": "Zelená",
|
||||
"SubtitleLightGray": "Svetlo sivá",
|
||||
"SubtitleRed": "Červená",
|
||||
"SubtitleYellow": "Žltá",
|
||||
"SubtitleWhite": "Biela",
|
||||
"Select": "Vybrať",
|
||||
"EnableAudioNormalization": "Normalizácia hlasitosti",
|
||||
"GetThePlugin": "Získať zásuvný modul",
|
||||
"LabelParallelImageEncodingLimit": "Limit paralelného kódovania obrázkov",
|
||||
"Notifications": "Upozornenia",
|
||||
"NotificationsMovedMessage": "Funkcia upozornení bola presunutá do zásuvného modulu Webhook.",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNames": "Preferovať vložené názvy pred názvami súborov pre doplnky",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Doplnky väčšinou majú totožní vložení názov ako nadriadená položka. Zaškrtnutím ich môžete napriek uprednostniť.",
|
||||
"SubtitleMagenta": "Fialová",
|
||||
"LabelEnableAudioVbrHelp": "Premenlivý bitový tok ponúka lepší pomer medzi kvalitou a priemerným bitovým tokom, ale niekdy môže spôsobiť dodatočné načítavanie alebo problémy s kompatibilitou.",
|
||||
"MenuClose": "Zatvoriť ponuku",
|
||||
"UserMenu": "Užívateľská ponuka",
|
||||
"LabelEnableLUFSScan": "Povoliť skenovanie LUFS",
|
||||
"LabelEnableLUFSScanHelp": "Povoliť skenovanie LUFS pre hudbu (Predlžuje skenovanie a je náročnejšie na výkon).",
|
||||
"MenuOpen": "Otvoriť ponuku"
|
||||
}
|
||||
|
|
|
@ -269,7 +269,7 @@
|
|||
"AlwaysPlaySubtitlesHelp": "Dil tercihi ile eşleşen altyazılar, ses diline bakılmaksızın yüklenir.",
|
||||
"AnyLanguage": "Herhangi bir dil",
|
||||
"Anytime": "İstediğin zaman",
|
||||
"AroundTime": "Civarında",
|
||||
"AroundTime": "Civarında {0}",
|
||||
"Art": "Temiz sanat",
|
||||
"AsManyAsPossible": "Mümkün olduğunca çok",
|
||||
"Ascending": "Artan",
|
||||
|
@ -881,7 +881,7 @@
|
|||
"Menu": "Menü",
|
||||
"EnableBlurHashHelp": "Hala yüklenmekte olan resimler benzersiz bir yer tutucuyla görüntülenecektir.",
|
||||
"EnableBlurHash": "Resimler için bulanık yer tutucuları etkinleştir",
|
||||
"AllowTonemappingHelp": "Ton eşleme, orijinal sahneyi temsil etmek için çok önemli bilgiler olan görüntü ayrıntılarını ve renkleri korurken bir videonun dinamik aralığını HDR'den SDR'ye dönüştürebilir. Şu anda yalnızca HDR10 veya HLG videolar ile çalışır. İlgili OpenCL veya CUDA çalışma zamanını gerektirir.",
|
||||
"AllowTonemappingHelp": "Ton eşleme, orijinal sahneyi temsil etmek için çok önemli bilgiler olan görüntü ayrıntılarını ve renkleri korurken bir videonun dinamik aralığını HDR'den SDR'ye dönüştürebilir. Şu anda yalnızca 10bit HDR10,HLG ve DoVi videolarla çalışmaktadır. Bunun için ilgili OpenCL veya CUDA çalışma zamanı gerekir.",
|
||||
"LabelAutomaticDiscovery": "Otomatik Keşfetmeyi Etkinleştir",
|
||||
"LabelAutoDiscoveryTracingHelp": "Etkinleştirildiğinde, otomatik keşfetme bağlantı noktasına gelen paketler günlüğe kaydedilir.",
|
||||
"LabelAutoDiscoveryTracing": "Otomatik Keşfetme izlemesini etkinleştirin.",
|
||||
|
@ -1148,7 +1148,7 @@
|
|||
"HeaderContinueReading": "Okumaya devam et",
|
||||
"DisableCustomCss": "Sunucu tarafından sağlanan özel CSS kodunu devre dışı bırak",
|
||||
"DirectPlayHelp": "Kaynak dosyası bu istemci ile tamamen uyumlu ve oturum dosyayı değişiklik yapmadan alıyor.",
|
||||
"OptionCaptionInfoExSamsung": "CaptionInfoEx (Samsung)",
|
||||
"OptionCaptionInfoExSamsung": "Altyazı Bilgisi Ex (Samsung)",
|
||||
"OptionBluray": "BD",
|
||||
"LabelAutomaticallyAddToCollection": "Otomatik olarak koleksiyona ekle",
|
||||
"HeaderSyncPlayPlaybackSettings": "Oynatma",
|
||||
|
@ -1589,9 +1589,9 @@
|
|||
"MediaInfoVideoRangeType": "Videonun aralık türü",
|
||||
"LabelVideoRangeType": "Videonun aralık türü",
|
||||
"VideoRangeTypeNotSupported": "Videonun aralık türü desteklenmiyor",
|
||||
"LabelVppTonemappingContrastHelp": "VPP ton eşlemede kontrast kazancını uygulayın. Önerilen ve varsayılan değerler 1,2 ve 1'dir.",
|
||||
"LabelVppTonemappingContrastHelp": "VPP ton eşlemesinde kontrast kazancı uygulayın. Hem önerilen hem de varsayılan değerler 1'dir.",
|
||||
"LabelVppTonemappingContrast": "VPP Ton eşleme kontrast kazancı",
|
||||
"LabelVppTonemappingBrightnessHelp": "VPP ton eşlemesinde parlaklık kazancını uygulayın. Hem önerilen hem de varsayılan değerler 0'dır.",
|
||||
"LabelVppTonemappingBrightnessHelp": "VPP ton eşlemesinde parlaklık kazancı uygulayın. Önerilen ve varsayılan değerler 16 ve 0'dır.",
|
||||
"LabelVppTonemappingBrightness": "VPP Ton eşleme parlaklık kazancı",
|
||||
"EnableSplashScreen": "Açılış ekranını etkinleştir",
|
||||
"EnableEnhancedNvdecDecoderHelp": "Deneysel NVDEC uyarlanması, kod çözme hatalarıyla karşılaşmadığınız sürece bu seçeneği etkinleştirmeyin.",
|
||||
|
@ -1674,5 +1674,61 @@
|
|||
"TabContainers": "Barındırıcılar",
|
||||
"OptionDateShowAdded": "Dizi Eklenme Tarihi",
|
||||
"OptionDateEpisodeAdded": "Bölüm Eklenme Tarihi",
|
||||
"DownloadAll": "Hepsini indir"
|
||||
"DownloadAll": "Hepsini indir",
|
||||
"AllowCollectionManagement": "Bu kullanıcının koleksiyonları yönetmesine izin ver",
|
||||
"Experimental": "Deneysel",
|
||||
"Featurette": "Tanıtım",
|
||||
"Select": "Seçiniz",
|
||||
"EnableCardLayout": "Görsel Kart kutusunu göster",
|
||||
"TonemappingModeHelp": "Ton eşleme modunu seçin. Vurguların patladığını görürseniz RGB moduna geçmeyi deneyin.",
|
||||
"Unreleased": "Henüz yayınlanmadı",
|
||||
"LabelTonemappingMode": "Ton eşleme modu",
|
||||
"GetThePlugin": "Eklentiyi edinin",
|
||||
"HeaderDummyChapter": "Bölüm Görüntüleri",
|
||||
"HeaderPerformance": "Performans",
|
||||
"EnableAudioNormalizationHelp": "Ses normalizasyonu, ortalamayı istenen seviyede (-18dB) tutmak için sabit bir kazanç ekleyecektir.",
|
||||
"EnableAudioNormalization": "Ses Normalizasyonu",
|
||||
"HeaderRecordingMetadataSaving": "Meta Verilerin Kaydedilmesi",
|
||||
"Short": "Kısa",
|
||||
"LabelDummyChapterDurationHelp": "Taklit bölümler arasındaki aralık. Taklit bölüm oluşturmayı devre dışı bırakmak için 0 olarak ayarlayın. Bunu değiştirmenin mevcut taklit bölümler üzerinde hiçbir etkisi olmayacaktır.",
|
||||
"LabelChapterImageResolution": "Çözünürlük",
|
||||
"ResolutionMatchSource": "Eşleşme Kaynağı",
|
||||
"SaveRecordingImages": "EPG görüntülerini kaydet",
|
||||
"SubtitleBlack": "Siyah",
|
||||
"SubtitleCyan": "Cam göbeği",
|
||||
"SubtitleRed": "Kırmızı",
|
||||
"PasswordRequiredForAdmin": "Yönetici hesapları için bir parola gereklidir.",
|
||||
"SaveRecordingImagesHelp": "EPG listeleme sağlayıcısından görüntüleri medyanın yanına kaydedin.",
|
||||
"StereoDownmixAlgorithmHelp": "Çok kanallı sesi stereoya düşürmek için kullanılan algoritma.",
|
||||
"SubtitleGray": "Gri",
|
||||
"SubtitleMagenta": "Eflatun",
|
||||
"SubtitleYellow": "Sarı",
|
||||
"UserMenu": "Kullanıcı Menüsü",
|
||||
"Studio": "Stüdyo",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Ekstralar genellikle ana adla aynı gömülü ada sahiptir, yine de gömülü başlıkları kullanmak için bunu kontrol edin.",
|
||||
"PreferEmbeddedExtrasTitlesOverFileNames": "Ekstralar için dosya adları yerine gömülü başlıkları tercih edin",
|
||||
"MessageNoItemsAvailable": "Şu anda hiçbir ürün mevcut değildir.",
|
||||
"LabelSyncPlayNoGroups": "Mevcut grup yok",
|
||||
"MessageNoFavoritesAvailable": "Şu anda hiçbir favori mevcut değildir.",
|
||||
"SaveRecordingNFO": "EPG meta verilerini NFO'ya kaydet",
|
||||
"SaveRecordingNFOHelp": "EPG listeleme sağlayıcısından gelen meta verileri medya ile birlikte kaydedin.",
|
||||
"SubtitleBlue": "Mavi",
|
||||
"SubtitleGreen": "Yeşil",
|
||||
"SubtitleLightGray": "Açık Gri",
|
||||
"SubtitleWhite": "Beyaz",
|
||||
"SecondarySubtitles": "İkincil Altyazılar",
|
||||
"LabelStereoDownmixAlgorithm": "Stereo Karıştırma Algoritması",
|
||||
"LabelEnableAudioVbr": "VBR ses kodlamasını etkinleştir",
|
||||
"MenuClose": "Menüyü Kapat",
|
||||
"MessageRenameMediaFolder": "Bir medya kitaplığını yeniden adlandırmak tüm meta verilerin kaybolmasına neden olacaktır, dikkatli olun.",
|
||||
"Notifications": "Bildirimler",
|
||||
"NotificationsMovedMessage": "Bildirim işlevi Webhook eklentisine taşındı.",
|
||||
"MenuOpen": "Açık Menü",
|
||||
"LabelEnableAudioVbrHelp": "Değişken bit hızı, ortalama bit hızı oranına göre daha iyi kalite sunar, ancak bazı nadir durumlarda arabelleğe alma ve uyumluluk sorunlarına neden olabilir.",
|
||||
"LabelEnableLUFSScan": "LUFS taramasını etkinleştir",
|
||||
"LabelEnableLUFSScanHelp": "Müzik için LUFS taramasını etkinleştirin (Bu daha uzun sürecek ve daha fazla kaynak gerektirecektir).",
|
||||
"LabelDummyChapterDuration": "Aralık",
|
||||
"LabelChapterImageResolutionHelp": "Çıkarılan bölüm görüntülerinin çözünürlüğü. Bunu değiştirmenin mevcut sahte bölümler üzerinde hiçbir etkisi olmayacaktır.",
|
||||
"LabelParallelImageEncodingLimit": "Paralel görüntü kodlama sınırı",
|
||||
"LabelParallelImageEncodingLimitHelp": "Paralel olarak çalışmasına izin verilen maksimum görüntü kodlaması miktarı. Bunu 0 olarak ayarlamak, sistem özelliklerinize göre bir sınır seçecektir."
|
||||
}
|
||||
|
|
|
@ -414,7 +414,7 @@
|
|||
"HeaderChannelAccess": "Доступ до каналів",
|
||||
"Framerate": "Частота кадрів",
|
||||
"EnableThemeSongsHelp": "Відтворювання тематичної музики у фоновому режимі під час перегляду медіатеки.",
|
||||
"DirectPlayHelp": "Вихідний файл повністю сумісний з цим клієнтом, а сеанс отримує файл без модифікацій.",
|
||||
"DirectPlayHelp": "Вихідний файл повністю сумісний з цим клієнтом і сеанс отримує файл без модифікацій.",
|
||||
"DeviceAccessHelp": "Це стосується лише пристроїв, які можна однозначно ідентифікувати та не блокують доступ до браузера. Фільтрування доступу користувацьких пристроїв не дозволить їм використовувати нові пристрої, доки вони не будуть затверджені тут.",
|
||||
"DeinterlaceMethodHelp": "Оберіть метод деінтерлейсингу, який використовуватиметься під час програмного перекодування вмісту. Якщо ввімкнено апаратне прискорення з підтримкою апаратного деінтерлейсингу, воно буде використовуватись натомість.",
|
||||
"MusicVideos": "Відеокліпи",
|
||||
|
@ -1151,7 +1151,7 @@
|
|||
"MessageReenableUser": "Щоб повторно ввімкнути, дивіться нижче",
|
||||
"MessagePluginInstallError": "Під час встановлення плагіна сталася помилка.",
|
||||
"MessagePluginInstalled": "Плагін успішно встановлено. Щоб зміни набули чинності, сервер потрібно буде перезапустити.",
|
||||
"MessagePluginInstallDisclaimer": "Плагіни, створені учасниками спільноти, — це чудовий спосіб покращити ваш досвід за допомогою додаткових функцій і переваг. Перед встановленням зверніть увагу на наслідки, які вони можуть мати на вашому сервері, наприклад, триваліші сканування медіатек, додаткова фонова обробка та зниження стабільності системи.",
|
||||
"MessagePluginInstallDisclaimer": "ПОПЕРЕДЖЕННЯ: Встановлення сторонніх плагінів несе певні ризики. Вони можуть містити нестабільний або шкідливий код і можуть змінюватися в будь-який час. Встановлюйте плагіни лише від авторів, яким ви довіряєте, і будь ласка, пам'ятайте про потенційні наслідки, які вони можуть мати, зокрема зовнішні запити до служб, довше сканування бібліотек або додаткову фонову обробку.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "Щоб налаштувати цей плагін, увійдіть безпосередньо на свій локальний сервер.",
|
||||
"MessagePleaseWait": "Будь ласка, зачекайте. Це може зайняти хвилину.",
|
||||
"MessagePleaseEnsureInternetMetadata": "Переконайтеся, що завантаження Інтернет-метаданих увімкнено.",
|
||||
|
@ -1303,7 +1303,7 @@
|
|||
"OptionPlainVideoItems": "Відображати всі відео як звичайні відеоелементи",
|
||||
"OptionPlainStorageFolders": "Відображати всі папки як звичайні папки зберігання",
|
||||
"OptionParentalRating": "Батьківський рейтинг",
|
||||
"OptionOnInterval": "На інтервалі",
|
||||
"OptionOnInterval": "З інтервалом",
|
||||
"OptionNew": "Новий…",
|
||||
"OptionMissingEpisode": "Відсутні епізоди",
|
||||
"OptionMaxActiveSessionsHelp": "Значення 0 вимкне функцію.",
|
||||
|
@ -1734,5 +1734,11 @@
|
|||
"GetThePlugin": "Отримати плагін",
|
||||
"NotificationsMovedMessage": "Функціонал сповіщень переїхав до плагіна Webhook.",
|
||||
"Notifications": "Сповіщення",
|
||||
"PasswordRequiredForAdmin": "Для облікових записів адміністратора потрібен пароль."
|
||||
"PasswordRequiredForAdmin": "Для облікових записів адміністратора потрібен пароль.",
|
||||
"LabelSyncPlayNoGroups": "Групи не доступні",
|
||||
"HeaderConfirmRepositoryInstallation": "Підтвердити встановлення репозиторію плагінів",
|
||||
"LabelDeveloper": "Розробник",
|
||||
"MessageRepositoryInstallDisclaimer": "ПОПЕРЕДЖЕННЯ: Встановлення стороннього репозиторію плагінів несе певні ризики. Він може містити нестабільний або шкідливий код і може змінюватися в будь-який час. Встановлюйте репозиторії лише від авторів, яким ви довіряєте.",
|
||||
"PleaseConfirmRepositoryInstallation": "Будь ласка, натисніть OK, щоб підтвердити, що ви прочитали вищезазначене і бажаєте продовжити встановлення репозиторію плагінів.",
|
||||
"Unknown": "Невідомо"
|
||||
}
|
||||
|
|
|
@ -1724,5 +1724,7 @@
|
|||
"Notifications": "Thông báo",
|
||||
"NotificationsMovedMessage": "Chức năng thông báo đã chuyển sang plugin Webhook.",
|
||||
"LabelEnableLUFSScan": "Bật tính năng quét LUFS",
|
||||
"LabelEnableLUFSScanHelp": "Bật tính năng quét LUFS để tìm nhạc (Việc này sẽ mất nhiều thời gian hơn và tốn nhiều tài nguyên hơn)."
|
||||
"LabelEnableLUFSScanHelp": "Bật tính năng quét LUFS để tìm nhạc (Việc này sẽ mất nhiều thời gian hơn và tốn nhiều tài nguyên hơn).",
|
||||
"PasswordRequiredForAdmin": "Cần có mật khẩu cho tài khoản quản trị viên.",
|
||||
"LabelSyncPlayNoGroups": "Không có nhóm nào"
|
||||
}
|
||||
|
|
|
@ -775,7 +775,7 @@
|
|||
"MessagePleaseEnsureInternetMetadata": "请确认已启用从网络上下载媒体资料的选项。",
|
||||
"MessagePleaseWait": "请稍等。这将花费大约1分钟的时间。",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "请直接登录你的本地服务器以设置这个插件。",
|
||||
"MessagePluginInstallDisclaimer": "安装社区成员构建的插件来获取额外的功能是增强您体验的一种很好的方式。但在安装之前请意识到他们可能会对你的服务器造成影响,如更长的媒体库扫描时间、额外的后台处理、以及系统稳定性的降低等。",
|
||||
"MessagePluginInstallDisclaimer": "警告:安装第三方插件有风险。它可能包含不稳定或恶意的代码,并且可能随时变化。请仅安装您信任的作者提供的插件,并请在安装之前意识到它可能造成的影响,例如对外部服务的查询、更长的媒体库扫描时间、以及额外的后台处理等。",
|
||||
"MessageReenableUser": "请参阅以下以重新启用",
|
||||
"MessageTheFollowingLocationWillBeRemovedFromLibrary": "以下媒体路径将从你的媒体库移除",
|
||||
"MessageUnableToConnectToServer": "现在无法连接所选择的服务器,请确保该服务器目前正在运行。",
|
||||
|
@ -1737,5 +1737,11 @@
|
|||
"GetThePlugin": "获取插件",
|
||||
"Notifications": "通知",
|
||||
"NotificationsMovedMessage": "通知功能已经转移到Webhook插件中。",
|
||||
"PasswordRequiredForAdmin": "管理员账户需要密码。"
|
||||
"PasswordRequiredForAdmin": "管理员账户需要密码。",
|
||||
"LabelSyncPlayNoGroups": "没有可用的组",
|
||||
"LabelDeveloper": "开发者",
|
||||
"MessageRepositoryInstallDisclaimer": "警告:安装第三方插件仓库有风险。它可能包含不稳定或恶意的代码,并且可能随时变化。请仅安装您信任的作者提供的插件仓库。",
|
||||
"PleaseConfirmRepositoryInstallation": "请点击确定键来确认您已经阅读了上述内容,并希望继续进行插件仓库的安装。",
|
||||
"Unknown": "未知",
|
||||
"HeaderConfirmRepositoryInstallation": "确认安装插件仓库"
|
||||
}
|
||||
|
|
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 {
|
||||
showTitle?: boolean;
|
||||
showYear?: boolean;
|
||||
|
@ -43,80 +27,3 @@ export interface ViewQuerySettings {
|
|||
NameStartsWith?: string | null;
|
||||
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;
|
||||
}
|
||||
|
|
3
src/types/library.ts
Normal file
3
src/types/library.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface LibraryViewProps {
|
||||
parentId: string | null;
|
||||
}
|
36
src/types/suggestionsSections.ts
Normal file
36
src/types/suggestionsSections.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
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 { CardOptions } from './cardOptions';
|
||||
|
||||
interface ParametersOptions {
|
||||
sortBy?: ItemSortBy[];
|
||||
sortOrder?: SortOrder[];
|
||||
includeItemTypes?: BaseItemKind[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -162,12 +162,17 @@ const config = {
|
|||
}
|
||||
},
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: [
|
||||
path.resolve(__dirname, 'node_modules/event-target-polyfill'),
|
||||
path.resolve(__dirname, 'node_modules/rvfc-polyfill'),
|
||||
path.resolve(__dirname, 'node_modules/@jellyfin/sdk'),
|
||||
path.resolve(__dirname, 'node_modules/@react-hook/latest'),
|
||||
path.resolve(__dirname, 'node_modules/@react-hook/passive-layout-effect'),
|
||||
path.resolve(__dirname, 'node_modules/@react-hook/resize-observer'),
|
||||
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/axios'),
|
||||
path.resolve(__dirname, 'node_modules/blurhash'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue