mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
Compare commits
128 Commits
feat/jukebox
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d57faa197 | |||
| 4129a8f56e | |||
| 1aa91fe2f5 | |||
| 5c399f7117 | |||
| 3c07f03651 | |||
| b26b6eab09 | |||
| b2579c031d | |||
| 98e2458a03 | |||
| b30e26ae7e | |||
| fd158b956a | |||
| 109788ebbb | |||
| ffdef596ad | |||
| 0a54f7c44c | |||
| d5d995de5f | |||
| 304c38db1e | |||
| ef631d12cc | |||
| 4006980b29 | |||
| f9c3c107bd | |||
| 7106b100ce | |||
| e2d56c70b1 | |||
| 1a930021b6 | |||
| 66699b9572 | |||
| 99be12e648 | |||
| dde4e1b33c | |||
| 88711eac2f | |||
| f21ca83179 | |||
| f43950874d | |||
| a3794158f0 | |||
| 7f52b31b40 | |||
| 18a864a049 | |||
| 4eac6457ea | |||
| df0d4b7032 | |||
| f0942c7795 | |||
| 396325f397 | |||
| 63015195b0 | |||
| e821397e6c | |||
| aae68853ef | |||
| f904aafd4a | |||
| 38b2508de6 | |||
| 8fee57157a | |||
| 6207cea9f1 | |||
| c299752e44 | |||
| 82e4f832eb | |||
| c8221c07ef | |||
| 710fc16f62 | |||
| 331cddcabb | |||
| b9a0d9b847 | |||
| 9c59a38f7a | |||
| b573999d33 | |||
| 35d8698ca0 | |||
| 23e4574667 | |||
| 7db15c7c72 | |||
| d94b220319 | |||
| acfc106f40 | |||
| 856400048b | |||
| a7c2a92f16 | |||
| fc3d700a57 | |||
| a60973ffee | |||
| a1114235d6 | |||
| 928b0b6f4d | |||
| 60d6d49eaa | |||
| 804a670bf1 | |||
| e51bb05564 | |||
| a21ee21652 | |||
| 8b2d162733 | |||
| cc76c9f31e | |||
| aa7a5037fa | |||
| 0acb1f54fc | |||
| 403ed8cae6 | |||
| 3db229ef68 | |||
| f0d22267c3 | |||
| 796e511626 | |||
| 06e757d3b2 | |||
| 0c8032d097 | |||
| 7cd86d1301 | |||
| 781df3ab06 | |||
| 88b9124185 | |||
| 48724f816c | |||
| 1a184a73de | |||
| e4b5cf36e1 | |||
| dff182cbc5 | |||
| fb8245539f | |||
| bb3cb4a6ad | |||
| fb2e30c484 | |||
| 73c5292cc1 | |||
| 800074dced | |||
| f78a572a3c | |||
| 97b20cec19 | |||
| fd833f683b | |||
| 076d9b3083 | |||
| 3a2a1b0dc8 | |||
| 20c19cac6f | |||
| 8205eeed22 | |||
| 5eb2cff6e9 | |||
| d822d9cd29 | |||
| 4142132ebc | |||
| fb2746323b | |||
| d9172efae9 | |||
| 8e04f98e26 | |||
| 51587fbb6b | |||
| cf06d69822 | |||
| 22751de2f6 | |||
| 04fbf5d3d2 | |||
| 936ba73fe4 | |||
| 05efd0f318 | |||
| ce570eddd2 | |||
| 5b1f269344 | |||
| 0806d9852a | |||
| c3e38d7133 | |||
| a322717e0e | |||
| dcb84dd442 | |||
| ac257a9dc1 | |||
| 25bfb65b6d | |||
| 96f38e597c | |||
| 383b728ddc | |||
| 003cfbdd6c | |||
| a67ae50d16 | |||
| ac3dcb5e17 | |||
| 833f82edff | |||
| 76f55111ec | |||
| f418bbfd2f | |||
| b02eba510d | |||
| 7a77b9bfe7 | |||
| c34b6774b9 | |||
| f3fe5b013a | |||
| 37be2cc8fa | |||
| e3c26aa5fa | |||
| e21f538aa4 |
@@ -4,9 +4,24 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- development
|
- development
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
wait-for-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Wait for Test workflow to complete
|
||||||
|
uses: lewagon/wait-on-check-action@v1.4.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
check-name: 'lint'
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
wait-interval: 10
|
||||||
|
allowed-conclusions: success
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
|
needs: wait-for-lint
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -105,18 +105,15 @@ services:
|
|||||||
feishin:
|
feishin:
|
||||||
container_name: feishin
|
container_name: feishin
|
||||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- SERVER_NAME=jellyfin # pre defined server name
|
- SERVER_NAME=jellyfin # pre-defined server name
|
||||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||||
- SERVER_TYPE=jellyfin # navidrome also works
|
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||||
- SERVER_URL= # http://address:port
|
- SERVER_URL= # http://address:port or https://address:port
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- UMASK=002
|
|
||||||
- TZ=America/Los_Angeles
|
|
||||||
ports:
|
ports:
|
||||||
- 9180:9180
|
- 9180:9180
|
||||||
restart: unless-stopped
|
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
version: '3.5'
|
|
||||||
services:
|
services:
|
||||||
feishin:
|
feishin:
|
||||||
container_name: feishin
|
container_name: feishin
|
||||||
image: ghcr.io/jeffvli/feishin:latest
|
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- SERVER_NAME=jellyfin # pre-defined server name
|
||||||
|
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||||
|
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||||
|
- SERVER_URL= # http://address:port or https://address:port
|
||||||
ports:
|
ports:
|
||||||
- 9180:9180
|
- 9180:9180
|
||||||
environment:
|
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||||
- SERVER_NAME=jellyfin # pre defined server name
|
|
||||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
|
||||||
- SERVER_TYPE=jellyfin # navidrome also works
|
|
||||||
- SERVER_URL= # http://address:port
|
|
||||||
Generated
+18279
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.22.0",
|
"version": "1.0.1",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -82,6 +82,8 @@
|
|||||||
"@xhayper/discord-rpc": "^1.3.0",
|
"@xhayper/discord-rpc": "^1.3.0",
|
||||||
"audiomotion-analyzer": "^4.5.1",
|
"audiomotion-analyzer": "^4.5.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"butterchurn": "^2.6.7",
|
||||||
|
"butterchurn-presets": "^2.4.7",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -121,6 +123,7 @@
|
|||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-player": "^2.16.0",
|
"react-player": "^2.16.0",
|
||||||
"react-router": "^7.9.6",
|
"react-router": "^7.9.6",
|
||||||
|
"react-split-pane": "^3.0.4",
|
||||||
"react-virtualized-auto-sizer": "^1.0.26",
|
"react-virtualized-auto-sizer": "^1.0.26",
|
||||||
"react-window": "1.8.11",
|
"react-window": "1.8.11",
|
||||||
"react-window-v2": "npm:react-window@^2.2.3",
|
"react-window-v2": "npm:react-window@^2.2.3",
|
||||||
|
|||||||
Generated
+65
-3
@@ -71,6 +71,12 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
|
butterchurn:
|
||||||
|
specifier: ^2.6.7
|
||||||
|
version: 2.6.7
|
||||||
|
butterchurn-presets:
|
||||||
|
specifier: ^2.4.7
|
||||||
|
version: 2.4.7
|
||||||
cheerio:
|
cheerio:
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
@@ -188,6 +194,9 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.9.6
|
specifier: ^7.9.6
|
||||||
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react-split-pane:
|
||||||
|
specifier: ^3.0.4
|
||||||
|
version: 3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react-virtualized-auto-sizer:
|
react-virtualized-auto-sizer:
|
||||||
specifier: ^1.0.26
|
specifier: ^1.0.26
|
||||||
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -2263,6 +2272,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
|
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
|
||||||
|
|
||||||
|
babel-runtime@6.26.0:
|
||||||
|
resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
@@ -2359,6 +2371,12 @@ packages:
|
|||||||
builder-util@26.0.11:
|
builder-util@26.0.11:
|
||||||
resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==}
|
resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==}
|
||||||
|
|
||||||
|
butterchurn-presets@2.4.7:
|
||||||
|
resolution: {integrity: sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg==}
|
||||||
|
|
||||||
|
butterchurn@2.6.7:
|
||||||
|
resolution: {integrity: sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg==}
|
||||||
|
|
||||||
cac@6.7.14:
|
cac@6.7.14:
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2555,6 +2573,10 @@ packages:
|
|||||||
core-js-compat@3.47.0:
|
core-js-compat@3.47.0:
|
||||||
resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
|
resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
|
||||||
|
|
||||||
|
core-js@2.6.12:
|
||||||
|
resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
|
||||||
|
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||||
|
|
||||||
@@ -2779,6 +2801,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
ecma-proposal-math-extensions@0.0.2:
|
||||||
|
resolution: {integrity: sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==}
|
||||||
|
|
||||||
ejs@3.1.10:
|
ejs@3.1.10:
|
||||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4621,6 +4646,13 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-split-pane@3.0.4:
|
||||||
|
resolution: {integrity: sha512-+QNayN8lsYhT87z0bH5yAuUocoqHlc3AQnw/+pGXMH2kG2+mSfNAR4fHhEdmweHLFjIyX811hh9sgCkiHXCYag==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react-style-singleton@2.2.3:
|
react-style-singleton@2.2.3:
|
||||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4688,6 +4720,9 @@ packages:
|
|||||||
regenerate@1.4.2:
|
regenerate@1.4.2:
|
||||||
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
||||||
|
|
||||||
|
regenerator-runtime@0.11.1:
|
||||||
|
resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==}
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.4:
|
regexp.prototype.flags@1.5.4:
|
||||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -7998,6 +8033,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
babel-runtime@6.26.0:
|
||||||
|
dependencies:
|
||||||
|
core-js: 2.6.12
|
||||||
|
regenerator-runtime: 0.11.1
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
balanced-match@2.0.0: {}
|
balanced-match@2.0.0: {}
|
||||||
@@ -8134,6 +8174,17 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
butterchurn-presets@2.4.7:
|
||||||
|
dependencies:
|
||||||
|
babel-runtime: 6.26.0
|
||||||
|
ecma-proposal-math-extensions: 0.0.2
|
||||||
|
lodash: 4.17.21
|
||||||
|
|
||||||
|
butterchurn@2.6.7:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
ecma-proposal-math-extensions: 0.0.2
|
||||||
|
|
||||||
cac@6.7.14: {}
|
cac@6.7.14: {}
|
||||||
|
|
||||||
cacache@16.1.3:
|
cacache@16.1.3:
|
||||||
@@ -8361,6 +8412,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.0
|
browserslist: 4.28.0
|
||||||
|
|
||||||
|
core-js@2.6.12: {}
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -8604,6 +8657,8 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
ecma-proposal-math-extensions@0.0.2: {}
|
||||||
|
|
||||||
ejs@3.1.10:
|
ejs@3.1.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
jake: 10.9.2
|
jake: 10.9.2
|
||||||
@@ -9526,7 +9581,7 @@ snapshots:
|
|||||||
|
|
||||||
i18next@24.2.3(typescript@5.8.3):
|
i18next@24.2.3(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.1
|
'@babel/runtime': 7.28.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
||||||
@@ -10588,6 +10643,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
|
react-split-pane@3.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0):
|
react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce: 1.0.1
|
get-nonce: 1.0.1
|
||||||
@@ -10598,7 +10658,7 @@ snapshots:
|
|||||||
|
|
||||||
react-textarea-autosize@8.5.9(@types/react@19.2.5)(react@19.1.0):
|
react-textarea-autosize@8.5.9(@types/react@19.2.5)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.1
|
'@babel/runtime': 7.28.4
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
use-composed-ref: 1.4.0(@types/react@19.2.5)(react@19.1.0)
|
use-composed-ref: 1.4.0(@types/react@19.2.5)(react@19.1.0)
|
||||||
use-latest: 1.3.0(@types/react@19.2.5)(react@19.1.0)
|
use-latest: 1.3.0(@types/react@19.2.5)(react@19.1.0)
|
||||||
@@ -10607,7 +10667,7 @@ snapshots:
|
|||||||
|
|
||||||
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.1
|
'@babel/runtime': 7.28.4
|
||||||
dom-helpers: 5.2.1
|
dom-helpers: 5.2.1
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
@@ -10672,6 +10732,8 @@ snapshots:
|
|||||||
|
|
||||||
regenerate@1.4.2: {}
|
regenerate@1.4.2: {}
|
||||||
|
|
||||||
|
regenerator-runtime@0.11.1: {}
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.4:
|
regexp.prototype.flags@1.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
|
|||||||
+52
-13
@@ -14,7 +14,8 @@
|
|||||||
"tracks": "$t(entity.track_other)",
|
"tracks": "$t(entity.track_other)",
|
||||||
"nowPlaying": "ara sona",
|
"nowPlaying": "ara sona",
|
||||||
"shared": "$t(entity.playlist_other) compartida",
|
"shared": "$t(entity.playlist_other) compartida",
|
||||||
"favorites": "$t(entity.favorite_other)"
|
"favorites": "$t(entity.favorite_other)",
|
||||||
|
"radio": "$t(entity.radioStation_other)"
|
||||||
},
|
},
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"relatedArtists": "$t(entity.artist_other) similars",
|
"relatedArtists": "$t(entity.artist_other) similars",
|
||||||
@@ -184,6 +185,9 @@
|
|||||||
},
|
},
|
||||||
"folderList": {
|
"folderList": {
|
||||||
"title": "$t(entity.folder_other)"
|
"title": "$t(entity.folder_other)"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "emissores de ràdio"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -301,7 +305,8 @@
|
|||||||
"sort": "ordre",
|
"sort": "ordre",
|
||||||
"gridRows": "files de la quadrícula",
|
"gridRows": "files de la quadrícula",
|
||||||
"tableColumns": "columnes de la taula",
|
"tableColumns": "columnes de la taula",
|
||||||
"itemsMore": "{{count}} més"
|
"itemsMore": "{{count}} més",
|
||||||
|
"countSelected": "{{count}} seleccionats"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "àlbum",
|
"album_one": "àlbum",
|
||||||
@@ -355,7 +360,13 @@
|
|||||||
"song_other": "cançons",
|
"song_other": "cançons",
|
||||||
"favorite_one": "preferit",
|
"favorite_one": "preferit",
|
||||||
"favorite_many": "preferits",
|
"favorite_many": "preferits",
|
||||||
"favorite_other": "preferits"
|
"favorite_other": "preferits",
|
||||||
|
"radioStation_one": "emissora de ràdio",
|
||||||
|
"radioStation_many": "emissores de ràdio",
|
||||||
|
"radioStation_other": "emissores de ràdio",
|
||||||
|
"radioStationWithCount_one": "{{count}} emissora de ràdio",
|
||||||
|
"radioStationWithCount_many": "{{count}} emissores de ràdio",
|
||||||
|
"radioStationWithCount_other": "{{count}} emissores de ràdio"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
@@ -445,6 +456,16 @@
|
|||||||
"input_played_optionAll": "totes les pistes",
|
"input_played_optionAll": "totes les pistes",
|
||||||
"input_played_optionUnplayed": "només les pistes sense reproduir",
|
"input_played_optionUnplayed": "només les pistes sense reproduir",
|
||||||
"input_played_optionPlayed": "només les pistes reproduïdes"
|
"input_played_optionPlayed": "només les pistes reproduïdes"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"success": "emissora de ràdio creada amb èxit",
|
||||||
|
"title": "crea una emissora de ràdio",
|
||||||
|
"input_homepageUrl": "URL de la pàgina d'inici",
|
||||||
|
"input_name": "nom",
|
||||||
|
"input_streamUrl": "URL de transmissió"
|
||||||
|
},
|
||||||
|
"saveQueue": {
|
||||||
|
"success": "cua de reproducció desada al servidor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
@@ -479,7 +500,13 @@
|
|||||||
"shuffle": "mescla",
|
"shuffle": "mescla",
|
||||||
"shuffleAll": "mescla-ho tot",
|
"shuffleAll": "mescla-ho tot",
|
||||||
"shuffleSelected": "mescla els seleccionats",
|
"shuffleSelected": "mescla els seleccionats",
|
||||||
"viewMore": "mostra'n més"
|
"viewMore": "mostra'n més",
|
||||||
|
"createRadioStation": "crea $t(entity.radioStation_one)",
|
||||||
|
"deleteRadioStation": "elimina $t(entity.radioStation_one)",
|
||||||
|
"addOrRemoveFromSelection": "afegeix o elimina de la selecció",
|
||||||
|
"selectRangeOfItems": "selecciona un interval d'elements",
|
||||||
|
"selectAll": "selecciona-ho tot",
|
||||||
|
"openApplicationDirectory": "obre el directori de l'aplicació"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))",
|
"language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))",
|
||||||
@@ -643,8 +670,6 @@
|
|||||||
"playbackStyle_optionNormal": "normal",
|
"playbackStyle_optionNormal": "normal",
|
||||||
"playButtonBehavior": "comportament del botó de reproducció",
|
"playButtonBehavior": "comportament del botó de reproducció",
|
||||||
"playButtonBehavior_description": "estableix el comportament predeterminat del botó de reproducció quan s'afegeixen cançons a la cua",
|
"playButtonBehavior_description": "estableix el comportament predeterminat del botó de reproducció quan s'afegeixen cançons a la cua",
|
||||||
"playerAlbumArtResolution": "resolució de la caràtula de l'àlbum al reproductor",
|
|
||||||
"playerAlbumArtResolution_description": "la resolució de la previsualització gran de la caràtula al reproductor. si és més alta, serà més nítida, però es carregarà més lent. el valor predeterminat 0 vol dir automàtic",
|
|
||||||
"playerbarOpenDrawer": "activa el reproductor en pantalla completa",
|
"playerbarOpenDrawer": "activa el reproductor en pantalla completa",
|
||||||
"playerbarOpenDrawer_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa",
|
"playerbarOpenDrawer_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa",
|
||||||
"remotePassword": "contrasenya del servidor de control remot",
|
"remotePassword": "contrasenya del servidor de control remot",
|
||||||
@@ -787,7 +812,9 @@
|
|||||||
"queryBuilderCustomFields_inputLabel": "discogràfica",
|
"queryBuilderCustomFields_inputLabel": "discogràfica",
|
||||||
"queryBuilderCustomFields_inputTag": "etiqueta",
|
"queryBuilderCustomFields_inputTag": "etiqueta",
|
||||||
"queryBuilderCustomFields": "camps personalitzats",
|
"queryBuilderCustomFields": "camps personalitzats",
|
||||||
"queryBuilderCustomFields_description": "afegeix camps personalitzats pel constructor de consultes"
|
"queryBuilderCustomFields_description": "afegeix camps personalitzats pel constructor de consultes",
|
||||||
|
"useThemeAccentColor": "fes servir el color d'accent del tema",
|
||||||
|
"useThemeAccentColor_description": "fes servir el color primari definit pel tema seleccionat en comptes del color d'accent personalitzat"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
@@ -953,8 +980,8 @@
|
|||||||
"repeat_all": "repetició",
|
"repeat_all": "repetició",
|
||||||
"shuffle": "reprodueix (mesclat)",
|
"shuffle": "reprodueix (mesclat)",
|
||||||
"shuffle_off": "reproducció aleatòria desactivada",
|
"shuffle_off": "reproducció aleatòria desactivada",
|
||||||
"addLast": "afegeix al final",
|
"addLast": "al final",
|
||||||
"addNext": "afegeix a continuació",
|
"addNext": "a continuació",
|
||||||
"favorite": "marcar com a preferida",
|
"favorite": "marcar com a preferida",
|
||||||
"mute": "silencia",
|
"mute": "silencia",
|
||||||
"next": "següent",
|
"next": "següent",
|
||||||
@@ -970,12 +997,15 @@
|
|||||||
"toggleFullscreenPlayer": "activa el reproductor de pantalla completa",
|
"toggleFullscreenPlayer": "activa el reproductor de pantalla completa",
|
||||||
"unfavorite": "elimina de preferits",
|
"unfavorite": "elimina de preferits",
|
||||||
"pause": "pausa",
|
"pause": "pausa",
|
||||||
"addLastShuffled": "afegeix al final (mesclat)",
|
"addLastShuffled": "al final (mesclat)",
|
||||||
"addNextShuffled": "afegeix a continuació (mesclat)",
|
"addNextShuffled": "a continuació (mesclat)",
|
||||||
"holdToShuffle": "mantén premut per mesclar",
|
"holdToShuffle": "mantén premut per mesclar",
|
||||||
"queueType": "tipus de cua",
|
"queueType": "tipus de cua",
|
||||||
"queueType_default": "predeterminat",
|
"queueType_default": "predeterminat",
|
||||||
"queueType_priority": "prioritat"
|
"queueType_priority": "prioritat",
|
||||||
|
"lyrics": "lletra",
|
||||||
|
"restoreQueueFromServer": "restaura la cua del servidor",
|
||||||
|
"saveQueueToServer": "desa la cua al servidor"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"credentialsRequired": "credencials requerides",
|
"credentialsRequired": "credencials requerides",
|
||||||
@@ -1001,7 +1031,10 @@
|
|||||||
"notificationDenied": "s'han negat els permisos per enviar notificacions. aquesta opció no té cap efecte",
|
"notificationDenied": "s'han negat els permisos per enviar notificacions. aquesta opció no té cap efecte",
|
||||||
"playbackError": "hi ha hagut un error en intentar reproduir el mitjà",
|
"playbackError": "hi ha hagut un error en intentar reproduir el mitjà",
|
||||||
"remoteDisableError": "hi ha hagut un error en intentar $t(common.disable) el servidor remot",
|
"remoteDisableError": "hi ha hagut un error en intentar $t(common.disable) el servidor remot",
|
||||||
"endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}"
|
"endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}",
|
||||||
|
"multipleServerSaveQueueError": "la cua de reproducció té una o més cançons que no són del servidor actual, cosa que no és compatible",
|
||||||
|
"saveQueueFailed": "error en desar la cua",
|
||||||
|
"settingsSyncError": "hi ha discrepàncies entre la configuració del renderitzador i el procés principal. reinicieu l'aplicació per aplicar els canvis"
|
||||||
},
|
},
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
"primary": {
|
"primary": {
|
||||||
@@ -1055,5 +1088,11 @@
|
|||||||
"queryBuilder": {
|
"queryBuilder": {
|
||||||
"standardTags": "etiquetes estàndard",
|
"standardTags": "etiquetes estàndard",
|
||||||
"customTags": "etiquetes personalitzades"
|
"customTags": "etiquetes personalitzades"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "min",
|
||||||
|
"secondShort": "s",
|
||||||
|
"hourShort": "h",
|
||||||
|
"dayShort": "d"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-8
@@ -39,7 +39,9 @@
|
|||||||
"holdToShuffle": "podržte pro zamíchání",
|
"holdToShuffle": "podržte pro zamíchání",
|
||||||
"lyrics": "texty",
|
"lyrics": "texty",
|
||||||
"restoreQueueFromServer": "obnovit frontu ze serveru",
|
"restoreQueueFromServer": "obnovit frontu ze serveru",
|
||||||
"saveQueueToServer": "uložit frontu na server"
|
"saveQueueToServer": "uložit frontu na server",
|
||||||
|
"artistRadio": "rádio umělce",
|
||||||
|
"trackRadio": "rádio skladby"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
||||||
@@ -207,8 +209,6 @@
|
|||||||
"passwordStore": "ukládání hesel / tajných klíčů",
|
"passwordStore": "ukládání hesel / tajných klíčů",
|
||||||
"mpvExtraParameters_help": "jeden na řádek",
|
"mpvExtraParameters_help": "jeden na řádek",
|
||||||
"homeConfiguration": "nastavení domovské stránky",
|
"homeConfiguration": "nastavení domovské stránky",
|
||||||
"playerAlbumArtResolution_description": "rozlišení náhledu obalu alba ve velkém přehrávači. větší hodnota znamená kvalitnější obrázek, ale může se déle načítat. výchozí hodnota je 0, což znamená automatické rozlišení",
|
|
||||||
"playerAlbumArtResolution": "rozlišení obalu alba v přehrávači",
|
|
||||||
"externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba",
|
"externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba",
|
||||||
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
||||||
"externalLinks": "zobrazit externí odkazy",
|
"externalLinks": "zobrazit externí odkazy",
|
||||||
@@ -349,7 +349,18 @@
|
|||||||
"logLevel_optionInfo": "informace",
|
"logLevel_optionInfo": "informace",
|
||||||
"logLevel_optionWarn": "varování",
|
"logLevel_optionWarn": "varování",
|
||||||
"useThemeAccentColor": "použít barvu motivu",
|
"useThemeAccentColor": "použít barvu motivu",
|
||||||
"useThemeAccentColor_description": "použít primární barvu definovanou ve zvoleném motivu namísto vlastní barvy rozhraní"
|
"useThemeAccentColor_description": "použít primární barvu definovanou ve zvoleném motivu namísto vlastní barvy rozhraní",
|
||||||
|
"artistRadioCount_description": "nastaví počet skladeb, které načíst pro rádio umělce a rádio skladby",
|
||||||
|
"artistRadioCount": "počet skladeb pro rádio umělce/skladby",
|
||||||
|
"imageResolution": "rozlišení obrázků",
|
||||||
|
"imageResolution_description": "rozlišení obrázků používaných napříč aplikací. nastavení hodnoty 0 použije nativní rozlišení obrázku",
|
||||||
|
"imageResolution_optionTable": "tabulka",
|
||||||
|
"imageResolution_optionItemCard": "karta položky",
|
||||||
|
"imageResolution_optionSidebar": "postranní lišta",
|
||||||
|
"imageResolution_optionHeader": "záhlaví",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "přehrávač na celé obrazovce",
|
||||||
|
"combinedLyricsAndVisualizer_description": "spojit texty a vizualizér do jednoho panelu",
|
||||||
|
"combinedLyricsAndVisualizer": "spojit texty a vizualizér v postranní liště přehrávače"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||||
@@ -385,7 +396,11 @@
|
|||||||
"holdToMoveToTop": "podržte pro přesunutí nahoru",
|
"holdToMoveToTop": "podržte pro přesunutí nahoru",
|
||||||
"holdToMoveToBottom": "podržte pro přesunutí dolů",
|
"holdToMoveToBottom": "podržte pro přesunutí dolů",
|
||||||
"createRadioStation": "vytvořit $t(entity.radioStation_one)",
|
"createRadioStation": "vytvořit $t(entity.radioStation_one)",
|
||||||
"deleteRadioStation": "odstranit $t(entity.radioStation_one)"
|
"deleteRadioStation": "odstranit $t(entity.radioStation_one)",
|
||||||
|
"openApplicationDirectory": "otevřít adresář aplikace",
|
||||||
|
"addOrRemoveFromSelection": "přidat nebo odebrat z výběru",
|
||||||
|
"selectRangeOfItems": "vyberte rozsah položek",
|
||||||
|
"selectAll": "vybrat vše"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "zpátky",
|
"backward": "zpátky",
|
||||||
@@ -502,7 +517,9 @@
|
|||||||
"tableColumns": "sloupce tabulky",
|
"tableColumns": "sloupce tabulky",
|
||||||
"itemsMore": "{{count}} dalších",
|
"itemsMore": "{{count}} dalších",
|
||||||
"noFilters": "nejsou nastaveny žádné filtry",
|
"noFilters": "nejsou nastaveny žádné filtry",
|
||||||
"view": "zobrazit"
|
"view": "zobrazit",
|
||||||
|
"countSelected": "vybráno {{count}}",
|
||||||
|
"retry": "zkusit znovu"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -634,7 +651,10 @@
|
|||||||
"badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje",
|
"badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje",
|
||||||
"notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv",
|
"notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv",
|
||||||
"multipleServerSaveQueueError": "fronta přehrávání má jednu nebo více skladeb, které nejsou z aktuálního serveru. tato funkce není podporována",
|
"multipleServerSaveQueueError": "fronta přehrávání má jednu nebo více skladeb, které nejsou z aktuálního serveru. tato funkce není podporována",
|
||||||
"saveQueueFailed": "nepodařilo se uložit frontu"
|
"saveQueueFailed": "nepodařilo se uložit frontu",
|
||||||
|
"settingsSyncError": "byly zjištěny nesrovnalosti mezi nastavením v rendereru a hlavním procesem. restartujte aplikaci, aby se změny projevily",
|
||||||
|
"noNetwork": "server je nedostupný",
|
||||||
|
"noNetworkDescription": "k tomuto serveru se nepodařilo připojit"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "nejvíce přehráváno",
|
"mostPlayed": "nejvíce přehráváno",
|
||||||
@@ -804,7 +824,8 @@
|
|||||||
"transcoding": "překódování",
|
"transcoding": "překódování",
|
||||||
"discord": "discord",
|
"discord": "discord",
|
||||||
"playerFilters": "filtry přehrávače",
|
"playerFilters": "filtry přehrávače",
|
||||||
"logger": "protokol"
|
"logger": "protokol",
|
||||||
|
"lyricsDisplay": "zobrazení textů"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
@@ -970,6 +991,11 @@
|
|||||||
"input_homepageUrl": "adresa domovské stránky",
|
"input_homepageUrl": "adresa domovské stránky",
|
||||||
"input_name": "název",
|
"input_name": "název",
|
||||||
"input_streamUrl": "adresa streamu"
|
"input_streamUrl": "adresa streamu"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "exportovat texty",
|
||||||
|
"input_synced": "exportovat synchronizované texty",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -1084,5 +1110,157 @@
|
|||||||
"notInPlaylist": "není v",
|
"notInPlaylist": "není v",
|
||||||
"notInTheLast": "není v posledním",
|
"notInTheLast": "není v posledním",
|
||||||
"startsWith": "začíná na"
|
"startsWith": "začíná na"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "min.",
|
||||||
|
"secondShort": "s",
|
||||||
|
"hourShort": "h.",
|
||||||
|
"dayShort": "den"
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"visualizerType": "Typ vizualizéru",
|
||||||
|
"cyclePresets": "Cyklicky procházet předvolby",
|
||||||
|
"cycleTime": "Čas cyklování (sekundy)",
|
||||||
|
"includeAllPresets": "Zahrnout všechny předvolby",
|
||||||
|
"ignoredPresets": "Ignorované předvolby",
|
||||||
|
"selectedPresets": "Vybrané předvolby",
|
||||||
|
"randomizeNextPreset": "Náhodně vybrat další předvolbu",
|
||||||
|
"blendTime": "Prolnout čas",
|
||||||
|
"presets": "Předvolby",
|
||||||
|
"selectPreset": "Vybrat předvolbu",
|
||||||
|
"applyPreset": "Použít předvolbu",
|
||||||
|
"saveAsPreset": "Uložit jako předvolbu",
|
||||||
|
"updatePreset": "Aktualizovat předvolbu",
|
||||||
|
"copyConfiguration": "Kopírovat konfiguraci",
|
||||||
|
"pasteConfiguration": "Vložit konfiguraci",
|
||||||
|
"pasteConfigurationPlaceholder": "Sem vložte konfiguraci JSON…",
|
||||||
|
"pasteFromClipboard": "Vložit ze schránky",
|
||||||
|
"applyConfiguration": "Použít konfiguraci",
|
||||||
|
"configCopied": "Konfigurace zkopírována do schránky",
|
||||||
|
"configCopyFailed": "Nepodařilo se zkopírovat konfiguraci",
|
||||||
|
"configPasted": "Konfigurace úspěšně použita",
|
||||||
|
"configPasteFailed": "Nepodařilo se použít konfiguraci. Zkontrolujte prosím formát.",
|
||||||
|
"configPasteReadFailed": "Nepodařilo se přečíst schránku",
|
||||||
|
"presetName": "Název předvolby",
|
||||||
|
"presetNamePlaceholder": "Zadejte název předvolby",
|
||||||
|
"general": "Obecné",
|
||||||
|
"mode": "Režim",
|
||||||
|
"mode1To8": "Režim 1–8",
|
||||||
|
"mode10": "Režim 10",
|
||||||
|
"barSpace": "Mezera mezi sloupci",
|
||||||
|
"lineWidth": "Šířka linky",
|
||||||
|
"fillAlpha": "Vyplnit alfu",
|
||||||
|
"channelLayout": "Rozložení kanálů",
|
||||||
|
"maxFPS": "Max. počet snímků za sekundu",
|
||||||
|
"opacity": "Neprůhlednost",
|
||||||
|
"customGradients": "Vlastní přechody",
|
||||||
|
"addCustomGradient": "Přidat vlastní přechod",
|
||||||
|
"gradientName": "Název přechodu",
|
||||||
|
"gradientNamePlaceholder": "Název přechodu",
|
||||||
|
"vertical": "Vertikální",
|
||||||
|
"horizontal": "Horizontální",
|
||||||
|
"colorStops": "Ukončení barev",
|
||||||
|
"addColor": "Přidat barvu",
|
||||||
|
"position": "Pozice",
|
||||||
|
"level": "Úroveň",
|
||||||
|
"remove": "Odstranit",
|
||||||
|
"custom": "Vlastní",
|
||||||
|
"builtIn": "Vestavěné",
|
||||||
|
"colors": "Barvy",
|
||||||
|
"colorMode": "Režim barev",
|
||||||
|
"gradient": "Přechod",
|
||||||
|
"gradientLeft": "Přechod zleva",
|
||||||
|
"gradientRight": "Přechod zprava",
|
||||||
|
"fft": "FFT",
|
||||||
|
"fftSize": "Velikost FFT",
|
||||||
|
"smoothing": "Vyhlazování",
|
||||||
|
"frequencyRangeAndScaling": "Rozsah a škálování frekvencí",
|
||||||
|
"minimumFrequency": "Minimální frekvence",
|
||||||
|
"maximumFrequency": "Maximální frekvence",
|
||||||
|
"frequencyScale": "Škála frekvence",
|
||||||
|
"sensitivity": "Citlivost",
|
||||||
|
"weightingFilter": "Filtr váhy",
|
||||||
|
"minimumDecibels": "Minimální decibely",
|
||||||
|
"maximumDecibels": "Maximální decibely",
|
||||||
|
"linearAmplitude": "Lineární amplituda",
|
||||||
|
"linearBoost": "Lineární zesílení",
|
||||||
|
"peakBehavior": "Chování ve špičce",
|
||||||
|
"showPeaks": "Zobrazit špičky",
|
||||||
|
"fadePeaks": "Prolnout špičky",
|
||||||
|
"peakLine": "Linka špiček",
|
||||||
|
"gravity": "Gravitace",
|
||||||
|
"peakFadeTime": "Čas pádu ze špičky (ms)",
|
||||||
|
"peakHoldTime": "Čas udržení na špičce (ms)",
|
||||||
|
"radialSpectrum": "Kruhové spektrum",
|
||||||
|
"radial": "Kruhové",
|
||||||
|
"radialInvert": "Kruhové invertované",
|
||||||
|
"spinSpeed": "Rychlost rotace",
|
||||||
|
"radius": "Poloměr",
|
||||||
|
"reflexMirror": "Reflexní zrcadlení",
|
||||||
|
"reflexFit": "Reflexní vyplnění",
|
||||||
|
"reflexRatio": "Reflexní poměr",
|
||||||
|
"reflexAlpha": "Reflexní alfa",
|
||||||
|
"reflexBrightness": "Reflexní jas",
|
||||||
|
"mirror": "Zrcadlení",
|
||||||
|
"miscellaneousSettings": "Různá nastavení",
|
||||||
|
"alphaBars": "Alfa sloupce",
|
||||||
|
"ansiBands": "ANSI sloupce",
|
||||||
|
"ledBars": "LED sloupce",
|
||||||
|
"trueLeds": "Pravé LED",
|
||||||
|
"lumiBars": "Lumi sloupce",
|
||||||
|
"outlineBars": "Obrysové sloupce",
|
||||||
|
"roundBars": "Zaoblené sloupce",
|
||||||
|
"lowResolution": "Nízké rozlišení",
|
||||||
|
"splitGradient": "Přechod rozdělení",
|
||||||
|
"showFPS": "Zobrazit FPS",
|
||||||
|
"showScaleX": "Zobrazit osu X",
|
||||||
|
"noteLabels": "Štítky not",
|
||||||
|
"showScaleY": "Zobrazit osu Y",
|
||||||
|
"options": {
|
||||||
|
"mode": {
|
||||||
|
"bars": "[0] Sloupce",
|
||||||
|
"circle": "[1] Kruh",
|
||||||
|
"wave": "[2] Vlna",
|
||||||
|
"rainbow": "[3] Duha",
|
||||||
|
"rings": "[4] Prstence",
|
||||||
|
"mirror": "[5] Zrcadlo",
|
||||||
|
"line": "[6] Linka",
|
||||||
|
"particles": "[7] Částice",
|
||||||
|
"fullOctave": "[8] Plná oktáva / 10 pásem",
|
||||||
|
"outlineBars": "[10] Obrysové sloupce"
|
||||||
|
},
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "Přechod",
|
||||||
|
"barIndex": "Index sloupce",
|
||||||
|
"barLevel": "Úroveň sloupce"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "Klasický",
|
||||||
|
"prism": "Prism",
|
||||||
|
"rainbow": "Duha",
|
||||||
|
"steelblue": "Ocelově modrá",
|
||||||
|
"orangered": "Oranžová"
|
||||||
|
},
|
||||||
|
"channelLayout": {
|
||||||
|
"single": "Jeden",
|
||||||
|
"dualCombined": "Duální kombinované",
|
||||||
|
"dualHorizontal": "Duální horizontální",
|
||||||
|
"dualVertical": "Duální vertikální"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"bark": "Bark",
|
||||||
|
"linear": "Lineární",
|
||||||
|
"log": "Log",
|
||||||
|
"mel": "Mel"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "Žádný",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-46
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "$t(entity.playlist_one) bearbeiten",
|
"editPlaylist": "$t(entity.playlist_one) bearbeiten",
|
||||||
"clearQueue": "Warteschlange leeren",
|
"clearQueue": "Wiedergabeliste leeren",
|
||||||
"addToFavorites": "Zu $t(entity.favorite_other) hinzufügen",
|
"addToFavorites": "Zu $t(entity.favorite_other) hinzufügen",
|
||||||
"addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen",
|
"addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen",
|
||||||
"createPlaylist": "$t(entity.playlist_one) erstellen",
|
"createPlaylist": "$t(entity.playlist_one) erstellen",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen",
|
"removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen",
|
||||||
"viewPlaylists": "$t(entity.playlist_other) anzeigen",
|
"viewPlaylists": "$t(entity.playlist_other) anzeigen",
|
||||||
"refresh": "$t(common.refresh)",
|
"refresh": "$t(common.refresh)",
|
||||||
"removeFromQueue": "Aus Warteschlange entfernen",
|
"removeFromQueue": "Aus Wiedergabeliste entfernen",
|
||||||
"setRating": "Bewerten",
|
"setRating": "Bewerten",
|
||||||
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
||||||
"removeFromFavorites": "Aus $t(entity.favorite_other) entfernen",
|
"removeFromFavorites": "Aus $t(entity.favorite_other) entfernen",
|
||||||
@@ -29,7 +29,11 @@
|
|||||||
"shuffleSelected": "Ausgewählte zufällig wiedergeben",
|
"shuffleSelected": "Ausgewählte zufällig wiedergeben",
|
||||||
"viewMore": "Mehr zeigen",
|
"viewMore": "Mehr zeigen",
|
||||||
"moveUp": "Nach oben bewegen",
|
"moveUp": "Nach oben bewegen",
|
||||||
"moveDown": "Nach unten bewegen"
|
"moveDown": "Nach unten bewegen",
|
||||||
|
"createRadioStation": "$t(entity.radioStation_one) erstellen",
|
||||||
|
"deleteRadioStation": "$t(entity.radioStation_one) löschen",
|
||||||
|
"selectAll": "alle auswählen",
|
||||||
|
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "zurück",
|
"backward": "zurück",
|
||||||
@@ -118,11 +122,11 @@
|
|||||||
"close": "schließen",
|
"close": "schließen",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"translation": "Übersetzung",
|
"translation": "Übersetzung",
|
||||||
"trackGain": "Track-Pegelverstärkung",
|
"trackGain": "Track Gain",
|
||||||
"trackPeak": "Track-Spitzenpegel",
|
"trackPeak": "Track Peak",
|
||||||
"codec": "Codec",
|
"codec": "Codec",
|
||||||
"albumPeak": "Album-Spitzenpegel",
|
"albumPeak": "Album-Spitzenpegel",
|
||||||
"albumGain": "Album-Pegelverstärkung",
|
"albumGain": "Album Gain",
|
||||||
"tags": "tags",
|
"tags": "tags",
|
||||||
"viewReleaseNotes": "Veröffentlichungsnotizen anzeigen",
|
"viewReleaseNotes": "Veröffentlichungsnotizen anzeigen",
|
||||||
"newVersion": "eine neue Version wurde installiert ({{version}})",
|
"newVersion": "eine neue Version wurde installiert ({{version}})",
|
||||||
@@ -145,7 +149,8 @@
|
|||||||
"recordLabel": "Plattenlabel",
|
"recordLabel": "Plattenlabel",
|
||||||
"slower": "langsamer",
|
"slower": "langsamer",
|
||||||
"releaseType": "Veröffentlichungsformat",
|
"releaseType": "Veröffentlichungsformat",
|
||||||
"view": "Betrachten"
|
"view": "Betrachten",
|
||||||
|
"countSelected": "{{count}} ausgewählt"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||||
@@ -167,11 +172,13 @@
|
|||||||
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
|
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
|
||||||
"invalidServer": "Ungültiger Server",
|
"invalidServer": "Ungültiger Server",
|
||||||
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
|
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
|
||||||
"badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Wahrscheinlich sehen Sie dieses Problem, wenn Sie einen Song in Ihrem Musikordner auf oberster Ebene haben. Jellyfin gruppiert nur Songs, wenn sie sich in einem Ordner befinden",
|
"badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Dieses Problem tritt meist auf, wenn sich ein Lied im Überordner befindet. Jellyfin gruppiert Tracks nur, wenn diese sich innerhalb eines Ordners befinden",
|
||||||
"networkError": "ein Netzwerkfehler ist aufgetreten",
|
"networkError": "ein Netzwerkfehler ist aufgetreten",
|
||||||
"openError": "datei kann nicht geöffnet werden",
|
"openError": "datei kann nicht geöffnet werden",
|
||||||
"badValue": "ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
|
"badValue": "ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
|
||||||
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt"
|
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt",
|
||||||
|
"saveQueueFailed": "Wiedergabeliste konnte nicht gespeichert werden",
|
||||||
|
"multipleServerSaveQueueError": "die Wiedergabeliste enthält einen oder mehrere Titel, die nicht vom aktuellen Server stammen. dies wird nicht unterstützt"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "Meistgespielt",
|
"mostPlayed": "Meistgespielt",
|
||||||
@@ -284,7 +291,7 @@
|
|||||||
"setExpiration": "Ablaufdatum setzen",
|
"setExpiration": "Ablaufdatum setzen",
|
||||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
||||||
"allowDownloading": "Herunterladen zulassen",
|
"allowDownloading": "Herunterladen zulassen",
|
||||||
"success": "Link in die Zwischenablage kopiert (oder hier klicken um zu öffnen)",
|
"success": "Link in die Zwischenablage kopiert (oder hier klicken, um zu öffnen)",
|
||||||
"createFailed": "fehler beim Teilen (Ist Teilen aktiviert?)"
|
"createFailed": "fehler beim Teilen (Ist Teilen aktiviert?)"
|
||||||
},
|
},
|
||||||
"privateMode": {
|
"privateMode": {
|
||||||
@@ -293,7 +300,7 @@
|
|||||||
"title": "Privatmodus"
|
"title": "Privatmodus"
|
||||||
},
|
},
|
||||||
"largeFetchConfirmation": {
|
"largeFetchConfirmation": {
|
||||||
"title": "Elemente der Warteschlange hinzufügen",
|
"title": "Elemente der Wiedergabeliste hinzufügen",
|
||||||
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
|
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
|
||||||
},
|
},
|
||||||
"shuffleAll": {
|
"shuffleAll": {
|
||||||
@@ -303,9 +310,19 @@
|
|||||||
"input_minYear": "ab Jahr",
|
"input_minYear": "ab Jahr",
|
||||||
"input_maxYear": "bis Jahr",
|
"input_maxYear": "bis Jahr",
|
||||||
"input_played_optionAll": "alle Tracks",
|
"input_played_optionAll": "alle Tracks",
|
||||||
"input_played_optionUnplayed": "nur ungespielte Tracks",
|
"input_played_optionUnplayed": "nur nicht gespielte Tracks",
|
||||||
"input_played_optionPlayed": "nur gespielte Tracks",
|
"input_played_optionPlayed": "nur gespielte Tracks",
|
||||||
"input_played": "Wiedergabefilter"
|
"input_played": "Wiedergabefilter"
|
||||||
|
},
|
||||||
|
"saveQueue": {
|
||||||
|
"success": "Wiedergabeliste auf Server gespeichert"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"success": "Radiosender erfolgreich erstellt",
|
||||||
|
"title": "Radiosender erstellen",
|
||||||
|
"input_homepageUrl": "Homepage URL",
|
||||||
|
"input_name": "Name",
|
||||||
|
"input_streamUrl": "Stream URL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -343,7 +360,11 @@
|
|||||||
"play_one": "{{count}} Wiedergabe",
|
"play_one": "{{count}} Wiedergabe",
|
||||||
"play_other": "{{count}} Wiedergaben",
|
"play_other": "{{count}} Wiedergaben",
|
||||||
"song_one": "Lied",
|
"song_one": "Lied",
|
||||||
"song_other": "Lieder"
|
"song_other": "Lieder",
|
||||||
|
"radioStation_one": "Radiosender",
|
||||||
|
"radioStation_other": "Radiosender",
|
||||||
|
"radioStationWithCount_one": "{{count}} Radiosender",
|
||||||
|
"radioStationWithCount_other": "{{count}} Radiosender"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -359,7 +380,17 @@
|
|||||||
"displayType": "Anzeigestil",
|
"displayType": "Anzeigestil",
|
||||||
"autoFitColumns": "automatisch Spalten einpassen",
|
"autoFitColumns": "automatisch Spalten einpassen",
|
||||||
"size_default": "Standard",
|
"size_default": "Standard",
|
||||||
"followCurrentSong": "aktuellem Titel folgen"
|
"followCurrentSong": "aktuellem Titel folgen",
|
||||||
|
"advancedSettings": "erweiterte Einstellungen",
|
||||||
|
"autosize": "automatische Größe",
|
||||||
|
"alignLeft": "linksbündig",
|
||||||
|
"alignCenter": "mittig",
|
||||||
|
"alignRight": "rechtsbündig",
|
||||||
|
"size_compact": "kompakt",
|
||||||
|
"size_large": "groß",
|
||||||
|
"pagination": "Seitenzahlen",
|
||||||
|
"pagination_itemsPerPage": "Elemente pro Seite",
|
||||||
|
"pagination_infinite": "unendlich"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"dateAdded": "Hinzugefügt am",
|
"dateAdded": "Hinzugefügt am",
|
||||||
@@ -387,7 +418,14 @@
|
|||||||
"title": "$t(common.title)",
|
"title": "$t(common.title)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"discNumber": "disk-Nummer",
|
"discNumber": "disk-Nummer",
|
||||||
"playCount": "Wiedergaben"
|
"playCount": "Wiedergaben",
|
||||||
|
"albumCount": "$t(entity.album_other)",
|
||||||
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"image": "Bild",
|
||||||
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
|
"songCount": "$t(entity.track_other)",
|
||||||
|
"genreBadge": "$t(entity.genre_one) (Abzeichen)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -413,7 +451,11 @@
|
|||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"songCount": "$t(entity.track_other)",
|
"songCount": "$t(entity.track_other)",
|
||||||
"trackNumber": "titel",
|
"trackNumber": "titel",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"bitDepth": "$t(common.bitDepth)",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"sampleRate": "$t(common.sampleRate)",
|
||||||
|
"owner": "Besitzer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -522,7 +564,8 @@
|
|||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "$t(entity.playlist_other) geteilt",
|
"shared": "$t(entity.playlist_other) geteilt",
|
||||||
"myLibrary": "meine bibliothek",
|
"myLibrary": "meine bibliothek",
|
||||||
"favorites": "$t(entity.favorite_other)"
|
"favorites": "$t(entity.favorite_other)",
|
||||||
|
"radio": "$t(entity.radioStation_other)"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"playbackTab": "Wiedergabe",
|
"playbackTab": "Wiedergabe",
|
||||||
@@ -538,7 +581,7 @@
|
|||||||
"application": "App",
|
"application": "App",
|
||||||
"queryBuilder": "Abfrage-Editor",
|
"queryBuilder": "Abfrage-Editor",
|
||||||
"theme": "Erscheinungsbild",
|
"theme": "Erscheinungsbild",
|
||||||
"controls": "Steuerung",
|
"controls": "Steuerelemente",
|
||||||
"sidebar": "Seitenleiste",
|
"sidebar": "Seitenleiste",
|
||||||
"scrobble": "Scrobbeln",
|
"scrobble": "Scrobbeln",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
@@ -601,14 +644,17 @@
|
|||||||
},
|
},
|
||||||
"playlist": {
|
"playlist": {
|
||||||
"reorder": "Neuanordnung nur bei Sortierung nach ID möglich"
|
"reorder": "Neuanordnung nur bei Sortierung nach ID möglich"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "Radiosender"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"next": "nächster",
|
"next": "nächster",
|
||||||
"addNext": "Als Nächstes spielen",
|
"addNext": "als Nächstes",
|
||||||
"play": "Abspielen",
|
"play": "Abspielen",
|
||||||
"muted": "stummgeschaltet",
|
"muted": "stummgeschaltet",
|
||||||
"addLast": "Als Letztes spielen",
|
"addLast": "als Letztes",
|
||||||
"mute": "Stumm",
|
"mute": "Stumm",
|
||||||
"playRandom": "Zufällige Wiedergabe",
|
"playRandom": "Zufällige Wiedergabe",
|
||||||
"previous": "Vorheriger",
|
"previous": "Vorheriger",
|
||||||
@@ -617,7 +663,7 @@
|
|||||||
"playbackFetchInProgress": "lieder werden geladen…",
|
"playbackFetchInProgress": "lieder werden geladen…",
|
||||||
"playbackSpeed": "Wiedergabegeschwindigkeit",
|
"playbackSpeed": "Wiedergabegeschwindigkeit",
|
||||||
"playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen",
|
"playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen",
|
||||||
"queue_clear": "Bereinige Warteschlange",
|
"queue_clear": "Wiedergabeliste bereinigen",
|
||||||
"repeat_all": "Alle wiederholen",
|
"repeat_all": "Alle wiederholen",
|
||||||
"repeat": "Wiederholen",
|
"repeat": "Wiederholen",
|
||||||
"queue_remove": "Ausgewählte entfernen",
|
"queue_remove": "Ausgewählte entfernen",
|
||||||
@@ -634,13 +680,15 @@
|
|||||||
"skip_forward": "vorspulen",
|
"skip_forward": "vorspulen",
|
||||||
"skip": "Überspringen",
|
"skip": "Überspringen",
|
||||||
"playSimilarSongs": "Ähnliche Lieder abspielen",
|
"playSimilarSongs": "Ähnliche Lieder abspielen",
|
||||||
"viewQueue": "Warteschlange anzeigen",
|
"viewQueue": "Wiedergabeliste anzeigen",
|
||||||
"addLastShuffled": "Als Letztes spielen (zufällige Wiedergabe)",
|
"addLastShuffled": "als Letztes (zufällige Wiedergabe)",
|
||||||
"addNextShuffled": "Als Nächstes spielen (zufällige Wiedergabe)",
|
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
|
||||||
"queueType_default": "Standard",
|
"queueType_default": "Standard",
|
||||||
"queueType_priority": "Priorität",
|
"queueType_priority": "Priorität",
|
||||||
"holdToShuffle": "Halten für Zufallswiedergabe",
|
"holdToShuffle": "Halten für Zufallswiedergabe",
|
||||||
"queueType": "Warteschlangentyp"
|
"queueType": "Wiedergabelistentyp",
|
||||||
|
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
||||||
|
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
||||||
@@ -716,7 +764,7 @@
|
|||||||
"themeLight_description": "Legt das Erscheinungsbild für den hellen Modus fest",
|
"themeLight_description": "Legt das Erscheinungsbild für den hellen Modus fest",
|
||||||
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
|
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
|
||||||
"hotkey_localSearch": "Suche auf Seite",
|
"hotkey_localSearch": "Suche auf Seite",
|
||||||
"hotkey_toggleQueue": "Warteschlange umschalten",
|
"hotkey_toggleQueue": "Wiedergabeliste umschalten",
|
||||||
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein Passwort verwenden, das Ihnen egal ist",
|
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein Passwort verwenden, das Ihnen egal ist",
|
||||||
"hotkey_rate5": "Bewertung 5 Sterne",
|
"hotkey_rate5": "Bewertung 5 Sterne",
|
||||||
"hotkey_playbackPrevious": "Vorheriger Track",
|
"hotkey_playbackPrevious": "Vorheriger Track",
|
||||||
@@ -727,18 +775,18 @@
|
|||||||
"playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll",
|
"playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll",
|
||||||
"mpvExecutablePath": "Pfad der ausführbaren MPV-Datei",
|
"mpvExecutablePath": "Pfad der ausführbaren MPV-Datei",
|
||||||
"hotkey_rate2": "Bewertung 2 Sterne",
|
"hotkey_rate2": "Bewertung 2 Sterne",
|
||||||
"playButtonBehavior_description": "Legt das Standardverhalten des Wiedergabe-Buttons fest, wenn Songs zur Warteschlange hinzugefügt werden",
|
"playButtonBehavior_description": "legt das Standardverhalten des Wiedergabe-Buttons fest, wenn Lieder zur Wiedergabeliste hinzugefügt werden",
|
||||||
"minimumScrobblePercentage_description": "die Mindestdauer in Prozent, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
"minimumScrobblePercentage_description": "die Mindestdauer in Prozent, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
||||||
"hotkey_rate4": "Bewertung 4 Sterne",
|
"hotkey_rate4": "Bewertung 4 Sterne",
|
||||||
"showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
|
"showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
|
||||||
"savePlayQueue": "Wiedergabe-Warteschlange speichern",
|
"savePlayQueue": "Wiedergabeliste speichern",
|
||||||
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
||||||
"skipPlaylistPage_description": "Gehe beim Navigieren zu einer Wiedergabeliste zu deren Titelseite und nicht zur Standardseite",
|
"skipPlaylistPage_description": "Gehe beim Navigieren zu einer Wiedergabeliste zu deren Titelseite und nicht zur Standardseite",
|
||||||
"fontType_description": "Die integrierte Schriftart wählt eine der von feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen",
|
"fontType_description": "Die integrierte Schriftart wählt eine der von feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen",
|
||||||
"playButtonBehavior": "Verhalten der Wiedergabetaste",
|
"playButtonBehavior": "Verhalten der Wiedergabetaste",
|
||||||
"volumeWheelStep": "Lautstärkeänderung mit Mausrad",
|
"volumeWheelStep": "Lautstärkeänderung mit Mausrad",
|
||||||
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
|
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
|
||||||
"sidePlayQueueStyle_description": "Legt den Stil der Wiedergabewarteliste in der Seitenleiste fest",
|
"sidePlayQueueStyle_description": "legt den Stil der Wiedergabeliste in der Seitenleiste fest",
|
||||||
"replayGainMode": "{{ReplayGain}} Modus",
|
"replayGainMode": "{{ReplayGain}} Modus",
|
||||||
"playbackStyle_optionNormal": "Normal",
|
"playbackStyle_optionNormal": "Normal",
|
||||||
"windowBarStyle": "Fensterleistenstil",
|
"windowBarStyle": "Fensterleistenstil",
|
||||||
@@ -769,7 +817,7 @@
|
|||||||
"gaplessAudio_optionWeak": "schwach (empfohlen)",
|
"gaplessAudio_optionWeak": "schwach (empfohlen)",
|
||||||
"minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)",
|
"minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)",
|
||||||
"hotkey_playbackStop": "Stoppen",
|
"hotkey_playbackStop": "Stoppen",
|
||||||
"savePlayQueue_description": "Speichert Wiedergabewarteschlange, wenn die Anwendung geschlossen wird, und stellt sie wieder her, wenn die Anwendung geöffnet wird",
|
"savePlayQueue_description": "speichert die Wiedergabeliste beim Schließen der Anwendung, und stellt diese wieder her, wenn die Anwendung geöffnet wird",
|
||||||
"useSystemTheme": "Nach Erscheinungsbild des Systems richten",
|
"useSystemTheme": "Nach Erscheinungsbild des Systems richten",
|
||||||
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
||||||
"fontType_optionSystem": "System Schriftart",
|
"fontType_optionSystem": "System Schriftart",
|
||||||
@@ -795,33 +843,33 @@
|
|||||||
"clearCache": "Browser-Zwischenspeicher löschen",
|
"clearCache": "Browser-Zwischenspeicher löschen",
|
||||||
"clearQueryCache": "feishins Zwischenspeicher leeren",
|
"clearQueryCache": "feishins Zwischenspeicher leeren",
|
||||||
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
|
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
|
||||||
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
|
"sidePlayQueueStyle": "Stil der Wiedergabeliste in der Seitenleiste",
|
||||||
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
||||||
"zoom": "Zoom",
|
"zoom": "Zoom",
|
||||||
"albumBackground": "Album Hintergrund",
|
"albumBackground": "Album Hintergrund",
|
||||||
"customCss": "Benutzerdefiniert css",
|
"customCss": "Benutzerdefiniertes CSS",
|
||||||
"homeConfiguration": "Startseite Konfiguration",
|
"homeConfiguration": "Startseite Konfiguration",
|
||||||
"lastfmApiKey": "{{lastfm}} API-Schlüssel",
|
"lastfmApiKey": "{{lastfm}} API-Schlüssel",
|
||||||
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für benötigt",
|
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für Albumcover benötigt",
|
||||||
"discordListening": "Status als hört zu anzeigen",
|
"discordListening": "Status als hört zu anzeigen",
|
||||||
"discordListening_description": "Status als hört zu statt als spielt anzeigen",
|
"discordListening_description": "Status als hört zu statt als spielt anzeigen",
|
||||||
"lastfm": "zeige last.fm links",
|
"lastfm": "zeige last.fm links",
|
||||||
"lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten",
|
"lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten",
|
||||||
"musicbrainz": "Zeig MusicBrainz links",
|
"musicbrainz": "Zeig MusicBrainz links",
|
||||||
"customCssEnable": "aktiviere Benutzerdefinierte css",
|
"customCssEnable": "benutzerdefiniertes CSS aktivieren",
|
||||||
"albumBackground_description": "fügt ein Hintergrundbild für die Albumseiten hinzu, welche das Albumcover zeigen",
|
"albumBackground_description": "fügt ein Hintergrundbild für die Albumseiten hinzu, welche das Albumcover zeigen",
|
||||||
"albumBackgroundBlur": "Größe der Album-Bildunschärfe",
|
"albumBackgroundBlur": "Größe der Album-Bildunschärfe",
|
||||||
"albumBackgroundBlur_description": "passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
|
"albumBackgroundBlur_description": "passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
|
||||||
"clearCacheSuccess": "Cache erfolgreich geleert",
|
"clearCacheSuccess": "Cache erfolgreich geleert",
|
||||||
"contextMenu": "Kontextmenü-Einstellungen (Rechtsklick)",
|
"contextMenu": "Kontextmenü-Einstellungen (Rechtsklick)",
|
||||||
"customCssEnable_description": "ermöglicht das Schreiben benutzerdefinierten CSS",
|
"customCssEnable_description": "erlaubt das Hinzufügen von benutzerdefiniertem CSS",
|
||||||
"artistBackground": "Künstler Hintergrundbild",
|
"artistBackground": "Künstler Hintergrundbild",
|
||||||
"artistBackground_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
"artistBackground_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
||||||
"artistConfiguration": "künstler Albumseite Konfiguration",
|
"artistConfiguration": "künstler Albumseite Konfiguration",
|
||||||
"buttonSize": "spielerleisten-Knopfgröße",
|
"buttonSize": "spielerleisten-Knopfgröße",
|
||||||
"buttonSize_description": "die Größe der Spieler-Knöpfe",
|
"buttonSize_description": "die Größe der Spieler-Knöpfe",
|
||||||
"hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus",
|
"hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus",
|
||||||
"replayGainFallback": "{{ReplayGain}} Rückgriff",
|
"replayGainFallback": "{{ReplayGain}} Alternative",
|
||||||
"replayGainClipping": "{{ReplayGain}} Clipping",
|
"replayGainClipping": "{{ReplayGain}} Clipping",
|
||||||
"exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren",
|
"exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren",
|
||||||
"exportImportSettings_control_exportText": "Einstellungen exportieren",
|
"exportImportSettings_control_exportText": "Einstellungen exportieren",
|
||||||
@@ -833,9 +881,7 @@
|
|||||||
"exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON",
|
"exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON",
|
||||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}",
|
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"playerAlbumArtResolution": "Auflösung des Albumcovers",
|
|
||||||
"imageAspectRatio": "Original Seitenverhältnis des Albumcovers verwenden",
|
"imageAspectRatio": "Original Seitenverhältnis des Albumcovers verwenden",
|
||||||
"playerAlbumArtResolution_description": "die Auflösung des Albumcovers im großen Player. Eine höhere Auflösung sorgt für ein schärferes Bild, kann jedoch das Laden verlangsamen. Standardwert: 0 (automatische Berechnung)",
|
|
||||||
"analyticsDisable": "Keine nutzungsbasierte Analyse",
|
"analyticsDisable": "Keine nutzungsbasierte Analyse",
|
||||||
"analyticsDisable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern",
|
"analyticsDisable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern",
|
||||||
"logLevel_optionDebug": "Debug",
|
"logLevel_optionDebug": "Debug",
|
||||||
@@ -844,11 +890,11 @@
|
|||||||
"logLevel_optionError": "Fehler",
|
"logLevel_optionError": "Fehler",
|
||||||
"logLevel_optionInfo": "Info",
|
"logLevel_optionInfo": "Info",
|
||||||
"logLevel_optionWarn": "Warnung",
|
"logLevel_optionWarn": "Warnung",
|
||||||
"autoDJ_description": "Füge automatisch ähnliche Lieder der Warteschlange hinzu",
|
"autoDJ_description": "füge automatisch ähnliche Lieder der Wiedergabeliste hinzu",
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_itemCount": "Anzahl",
|
"autoDJ_itemCount": "Anzahl",
|
||||||
"autoDJ_itemCount_description": "Die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Warteschlange hinzugefügt werden sollen",
|
"autoDJ_itemCount_description": "die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Wiedergabeliste hinzugefügt werden sollen",
|
||||||
"autoDJ_timing_description": "Die Anzahl der Lieder, die sich noch in der Warteschlange befinden, bevor Auto DJ ausgelöst wird",
|
"autoDJ_timing_description": "die Anzahl der Lieder, die sich noch in der Wiedergabeliste befinden, bevor Auto DJ ausgelöst wird",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
"discordDisplayType": "{{discord}} Presence Darstellungsart",
|
"discordDisplayType": "{{discord}} Presence Darstellungsart",
|
||||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz",
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz",
|
||||||
@@ -867,11 +913,11 @@
|
|||||||
"neteaseTranslation": "NetEase Übersetzungen aktivieren",
|
"neteaseTranslation": "NetEase Übersetzungen aktivieren",
|
||||||
"notify": "Benachrichtigungen aktivieren",
|
"notify": "Benachrichtigungen aktivieren",
|
||||||
"notify_description": "Zeigt Benachrichtigungen beim Titelwechsel",
|
"notify_description": "Zeigt Benachrichtigungen beim Titelwechsel",
|
||||||
"playerFilters": "Lieder in der Warteschlange filtern",
|
"playerFilters": "Lieder der Wiedergabeliste filtern",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"volumeWidth_description": "Die Breite des Lautstärkereglers",
|
"volumeWidth_description": "Die Breite des Lautstärkereglers",
|
||||||
"volumeWidth": "Lautstärkereglerbreite",
|
"volumeWidth": "Lautstärkereglerbreite",
|
||||||
"webAudio_description": "Web-Audio verwenden. Dies ermöglicht erweiterte Funktionen wie ReplayGain. Deaktiviere dies, wenn bei der Wiedergabe Probleme auftreten",
|
"webAudio_description": "Web-Audio verwenden. Dies ermöglicht erweiterte Funktionen wie ReplayGain. Deaktiviere die Option, falls bei der Wiedergabe Probleme auftreten",
|
||||||
"webAudio": "Web-Audio verwenden",
|
"webAudio": "Web-Audio verwenden",
|
||||||
"trayEnabled": "Info-Symbol anzeigen",
|
"trayEnabled": "Info-Symbol anzeigen",
|
||||||
"transcode": "Transkodierung aktivieren",
|
"transcode": "Transkodierung aktivieren",
|
||||||
@@ -889,7 +935,7 @@
|
|||||||
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
|
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
|
||||||
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
||||||
"crossfadeStyle": "Art der Überblende",
|
"crossfadeStyle": "Art der Überblende",
|
||||||
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: content und entfernte URLs sind unzulässig. Siehe Vorschau unten. Aufgrund von Bereinigung werden womöglich nicht gesetzte Felder angezeigt",
|
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Inhalte und Remote-URLs sind nicht zulässige Eigenschaften. Unten siehst du eine Vorschau deines Inhalts. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt",
|
||||||
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird",
|
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird",
|
||||||
"releaseChannel_optionBeta": "Beta",
|
"releaseChannel_optionBeta": "Beta",
|
||||||
"releaseChannel_optionLatest": "Stabil",
|
"releaseChannel_optionLatest": "Stabil",
|
||||||
@@ -903,11 +949,25 @@
|
|||||||
"exportImportSettings_destructiveWarning": "Das Importieren von Einstellungen ist irreversibel. Bitte lies die Hinweise oben sorgfältig durch, bevor du auf \"Importieren\" klickst!",
|
"exportImportSettings_destructiveWarning": "Das Importieren von Einstellungen ist irreversibel. Bitte lies die Hinweise oben sorgfältig durch, bevor du auf \"Importieren\" klickst!",
|
||||||
"followCurrentSong": "aktuellem Titel folgen",
|
"followCurrentSong": "aktuellem Titel folgen",
|
||||||
"followCurrentSong_description": "die Wiedergabeliste scrollt automatisch zum aktuellen Titel",
|
"followCurrentSong_description": "die Wiedergabeliste scrollt automatisch zum aktuellen Titel",
|
||||||
"playerFilters_description": "verhindert, dass Titel anhand der folgenden Kriterien zur Warteschlange hinzugefügt werden",
|
"playerFilters_description": "verhindert, dass Titel anhand der folgenden Kriterien zur Wiedergabeliste hinzugefügt werden",
|
||||||
"preferLocalLyrics_description": "lokale Songtexte gegenüber externen Quellen bevorzugen (sofern verfügbar)",
|
"preferLocalLyrics_description": "lokale Songtexte gegenüber externen Quellen bevorzugen (sofern verfügbar)",
|
||||||
"preferLocalLyrics": "Priorisiere lokale Songtexte",
|
"preferLocalLyrics": "Priorisiere lokale Songtexte",
|
||||||
"showLyricsInSidebar_description": "ein Bereich, in dem Songtexte angezeigt werden, wird der Wiedergabeliste hinzugefügt",
|
"showLyricsInSidebar_description": "ein Bereich, in dem Songtexte angezeigt werden, wird der Wiedergabeliste hinzugefügt",
|
||||||
"showLyricsInSidebar": "zeige Songtexte in der Player-Seitenleiste"
|
"showLyricsInSidebar": "zeige Songtexte in der Player-Seitenleiste",
|
||||||
|
"homeFeature_description": "steuert, ob das große Featured-Karussell auf der Startseite angezeigt wird",
|
||||||
|
"homeFeature": "Feature-Karussell",
|
||||||
|
"playerbarWaveformAlign_optionTop": "Oben",
|
||||||
|
"playerbarWaveformAlign_optionCenter": "Mitte",
|
||||||
|
"playerbarWaveformAlign_optionBottom": "Unten",
|
||||||
|
"translationApiKey_description": "API-Schlüssel für Übersetzungen (nur globale Service-Endpunkte)",
|
||||||
|
"translationApiKey": "API-Schlüssel für Übersetzungen",
|
||||||
|
"translationApiProvider_description": "API-Anbieter für Übersetzungen",
|
||||||
|
"translationApiProvider": "API-Anbieter für Übersetzungen",
|
||||||
|
"hotkey_navigateHome": "zurück zur Startseite",
|
||||||
|
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
|
||||||
|
"translationTargetLanguage": "Zielsprache der Übersetzung",
|
||||||
|
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
|
||||||
|
"queryBuilderCustomFields_inputTag": "Tag"
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||||
@@ -961,5 +1021,11 @@
|
|||||||
"soundtrack": "Soundtrack",
|
"soundtrack": "Soundtrack",
|
||||||
"spokenWord": "Gesprochenes Wort"
|
"spokenWord": "Gesprochenes Wort"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "Min",
|
||||||
|
"secondShort": "Sek",
|
||||||
|
"hourShort": "Std",
|
||||||
|
"dayShort": "Tag"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+180
-6
@@ -2,11 +2,14 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"addToFavorites": "add to $t(entity.favorite_other)",
|
"addToFavorites": "add to $t(entity.favorite_other)",
|
||||||
"addToPlaylist": "add to $t(entity.playlist_one)",
|
"addToPlaylist": "add to $t(entity.playlist_one)",
|
||||||
|
"addOrRemoveFromSelection": "add or remove from selection",
|
||||||
|
"selectRangeOfItems": "select a range of items",
|
||||||
"clearQueue": "clear queue",
|
"clearQueue": "clear queue",
|
||||||
"createPlaylist": "create $t(entity.playlist_one)",
|
"createPlaylist": "create $t(entity.playlist_one)",
|
||||||
"createRadioStation": "create $t(entity.radioStation_one)",
|
"createRadioStation": "create $t(entity.radioStation_one)",
|
||||||
"deletePlaylist": "delete $t(entity.playlist_one)",
|
"deletePlaylist": "delete $t(entity.playlist_one)",
|
||||||
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
||||||
|
"selectAll": "select all",
|
||||||
"deselectAll": "deselect all",
|
"deselectAll": "deselect all",
|
||||||
"downloadStarted": "started download of {{count}} items",
|
"downloadStarted": "started download of {{count}} items",
|
||||||
"editPlaylist": "edit $t(entity.playlist_one)",
|
"editPlaylist": "edit $t(entity.playlist_one)",
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"countSelected": "{{count}} selected",
|
||||||
"explicitStatus": "explicit status",
|
"explicitStatus": "explicit status",
|
||||||
"action_one": "action",
|
"action_one": "action",
|
||||||
"action_other": "actions",
|
"action_other": "actions",
|
||||||
@@ -115,6 +119,7 @@
|
|||||||
"quit": "quit",
|
"quit": "quit",
|
||||||
"random": "random",
|
"random": "random",
|
||||||
"rating": "rating",
|
"rating": "rating",
|
||||||
|
"retry": "retry",
|
||||||
"recordLabel": "record label",
|
"recordLabel": "record label",
|
||||||
"releaseType": "release type",
|
"releaseType": "release type",
|
||||||
"refresh": "refresh",
|
"refresh": "refresh",
|
||||||
@@ -208,6 +213,8 @@
|
|||||||
"mpvRequired": "MPV required",
|
"mpvRequired": "MPV required",
|
||||||
"multipleServerSaveQueueError": "the play queue has one or more songs which are not from the current server. this is not supported",
|
"multipleServerSaveQueueError": "the play queue has one or more songs which are not from the current server. this is not supported",
|
||||||
"networkError": "a network error occurred",
|
"networkError": "a network error occurred",
|
||||||
|
"noNetwork": "server unavailable",
|
||||||
|
"noNetworkDescription": "couldn't connect to this server",
|
||||||
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
||||||
"openError": "could not open file",
|
"openError": "could not open file",
|
||||||
"playbackError": "an error occurred when trying to play the media",
|
"playbackError": "an error occurred when trying to play the media",
|
||||||
@@ -268,10 +275,10 @@
|
|||||||
"explicitStatus": "$t(common.explicitStatus)"
|
"explicitStatus": "$t(common.explicitStatus)"
|
||||||
},
|
},
|
||||||
"datetime": {
|
"datetime": {
|
||||||
"minuteShort": "min",
|
"minuteShort": "m",
|
||||||
"secondShort": "sec",
|
"secondShort": "s",
|
||||||
"hourShort": "hr",
|
"hourShort": "h",
|
||||||
"dayShort": "day"
|
"dayShort": "d"
|
||||||
},
|
},
|
||||||
"filterOperator": {
|
"filterOperator": {
|
||||||
"after": "is after",
|
"after": "is after",
|
||||||
@@ -348,6 +355,11 @@
|
|||||||
"success": "$t(entity.playlist_one) updated successfully",
|
"success": "$t(entity.playlist_one) updated successfully",
|
||||||
"title": "edit $t(entity.playlist_one)"
|
"title": "edit $t(entity.playlist_one)"
|
||||||
},
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "export lyrics",
|
||||||
|
"input_synced": "export synced lyrics",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
|
},
|
||||||
"lyricSearch": {
|
"lyricSearch": {
|
||||||
"input_artist": "$t(entity.artist_one)",
|
"input_artist": "$t(entity.artist_one)",
|
||||||
"input_name": "$t(common.name)",
|
"input_name": "$t(common.name)",
|
||||||
@@ -398,6 +410,8 @@
|
|||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"about": "About {{artist}}",
|
"about": "About {{artist}}",
|
||||||
"appearsOn": "appears on",
|
"appearsOn": "appears on",
|
||||||
|
"groupingTypeAll": "all release types",
|
||||||
|
"groupingTypePrimary": "primary release types",
|
||||||
"recentReleases": "recent releases",
|
"recentReleases": "recent releases",
|
||||||
"viewDiscography": "view discography",
|
"viewDiscography": "view discography",
|
||||||
"relatedArtists": "related $t(entity.artist_other)",
|
"relatedArtists": "related $t(entity.artist_other)",
|
||||||
@@ -557,6 +571,7 @@
|
|||||||
"scrobble": "scrobble",
|
"scrobble": "scrobble",
|
||||||
"audio": "audio",
|
"audio": "audio",
|
||||||
"lyrics": "lyrics",
|
"lyrics": "lyrics",
|
||||||
|
"lyricsDisplay": "lyrics display",
|
||||||
"transcoding": "transcoding",
|
"transcoding": "transcoding",
|
||||||
"discord": "discord",
|
"discord": "discord",
|
||||||
"logger": "logger",
|
"logger": "logger",
|
||||||
@@ -590,6 +605,7 @@
|
|||||||
"addNext": "next",
|
"addNext": "next",
|
||||||
"addLastShuffled": "last (shuffled)",
|
"addLastShuffled": "last (shuffled)",
|
||||||
"addNextShuffled": "next (shuffled)",
|
"addNextShuffled": "next (shuffled)",
|
||||||
|
"artistRadio": "artist radio",
|
||||||
"holdToShuffle": "hold to shuffle",
|
"holdToShuffle": "hold to shuffle",
|
||||||
"favorite": "favorite",
|
"favorite": "favorite",
|
||||||
"lyrics": "lyrics",
|
"lyrics": "lyrics",
|
||||||
@@ -625,6 +641,7 @@
|
|||||||
"skip_forward": "skip forwards",
|
"skip_forward": "skip forwards",
|
||||||
"stop": "stop",
|
"stop": "stop",
|
||||||
"toggleFullscreenPlayer": "toggle fullscreen player",
|
"toggleFullscreenPlayer": "toggle fullscreen player",
|
||||||
|
"trackRadio": "track radio",
|
||||||
"unfavorite": "unfavorite",
|
"unfavorite": "unfavorite",
|
||||||
"pause": "pause",
|
"pause": "pause",
|
||||||
"viewQueue": "view queue"
|
"viewQueue": "view queue"
|
||||||
@@ -856,8 +873,15 @@
|
|||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playButtonBehavior": "play button behavior",
|
"playButtonBehavior": "play button behavior",
|
||||||
"playerAlbumArtResolution_description": "the resolution for the large player's album art preview. larger makes it look more crisp, but may slow loading down. defaults to 0, meaning auto",
|
"artistRadioCount_description": "sets the number of songs to fetch for artist radio and track radio",
|
||||||
"playerAlbumArtResolution": "player album art resolution",
|
"artistRadioCount": "artist/track radio count",
|
||||||
|
"imageResolution": "image resolution",
|
||||||
|
"imageResolution_description": "the resolution for the images used around the app. using a value of 0 will default to the native image resolution",
|
||||||
|
"imageResolution_optionTable": "table",
|
||||||
|
"imageResolution_optionItemCard": "item card",
|
||||||
|
"imageResolution_optionSidebar": "sidebar",
|
||||||
|
"imageResolution_optionHeader": "header",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "fullscreen player",
|
||||||
"playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player",
|
"playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player",
|
||||||
"playerbarOpenDrawer": "playerbar fullscreen toggle",
|
"playerbarOpenDrawer": "playerbar fullscreen toggle",
|
||||||
"playerbarSlider": "playerbar slider",
|
"playerbarSlider": "playerbar slider",
|
||||||
@@ -875,8 +899,12 @@
|
|||||||
"preferLocalLyrics": "prefer local lyrics",
|
"preferLocalLyrics": "prefer local lyrics",
|
||||||
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
|
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
|
||||||
"showLyricsInSidebar": "show lyrics in player sidebar",
|
"showLyricsInSidebar": "show lyrics in player sidebar",
|
||||||
|
"showRatings_description": "controls if the star ratings feature shows up in the interface",
|
||||||
|
"showRatings": "show star ratings",
|
||||||
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
||||||
"showVisualizerInSidebar": "show visualizer in player sidebar",
|
"showVisualizerInSidebar": "show visualizer in player sidebar",
|
||||||
|
"combinedLyricsAndVisualizer_description": "combine lyrics and visualizer into the same panel",
|
||||||
|
"combinedLyricsAndVisualizer": "combine lyrics and visualizer in player sidebar",
|
||||||
"preservePitch_description": "preserves pitch when modifying playback speed",
|
"preservePitch_description": "preserves pitch when modifying playback speed",
|
||||||
"preservePitch": "preserve pitch",
|
"preservePitch": "preserve pitch",
|
||||||
"audioFadeOnStatusChange": "audio fade on status change",
|
"audioFadeOnStatusChange": "audio fade on status change",
|
||||||
@@ -1074,5 +1102,151 @@
|
|||||||
"error_oneFileOnly": "Please only select 1 file",
|
"error_oneFileOnly": "Please only select 1 file",
|
||||||
"error_readingFile": "there has been an issue reading the file: {{errorMessage}}",
|
"error_readingFile": "there has been an issue reading the file: {{errorMessage}}",
|
||||||
"mainText": "drop a file here"
|
"mainText": "drop a file here"
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"visualizerType": "Visualizer Type",
|
||||||
|
"cyclePresets": "Cycle Presets",
|
||||||
|
"cycleTime": "Cycle Time (seconds)",
|
||||||
|
"includeAllPresets": "Include All Presets",
|
||||||
|
"ignoredPresets": "Ignored Presets",
|
||||||
|
"selectedPresets": "Selected Presets",
|
||||||
|
"randomizeNextPreset": "Randomize Next Preset",
|
||||||
|
"blendTime": "Blend Time",
|
||||||
|
"presets": "Presets",
|
||||||
|
"selectPreset": "Select Preset",
|
||||||
|
"applyPreset": "Apply Preset",
|
||||||
|
"saveAsPreset": "Save as Preset",
|
||||||
|
"updatePreset": "Update Preset",
|
||||||
|
"copyConfiguration": "Copy Configuration",
|
||||||
|
"pasteConfiguration": "Paste Configuration",
|
||||||
|
"pasteConfigurationPlaceholder": "Paste JSON configuration here...",
|
||||||
|
"pasteFromClipboard": "Paste from Clipboard",
|
||||||
|
"applyConfiguration": "Apply Configuration",
|
||||||
|
"configCopied": "Configuration copied to clipboard",
|
||||||
|
"configCopyFailed": "Failed to copy configuration",
|
||||||
|
"configPasted": "Configuration applied successfully",
|
||||||
|
"configPasteFailed": "Failed to apply configuration. Please check the format.",
|
||||||
|
"configPasteReadFailed": "Failed to read from clipboard",
|
||||||
|
"presetName": "Preset Name",
|
||||||
|
"presetNamePlaceholder": "Enter preset name",
|
||||||
|
"general": "General",
|
||||||
|
"mode": "Mode",
|
||||||
|
"mode1To8": "Mode 1 - 8",
|
||||||
|
"mode10": "Mode 10",
|
||||||
|
"barSpace": "Bar Space",
|
||||||
|
"lineWidth": "Line Width",
|
||||||
|
"fillAlpha": "Fill Alpha",
|
||||||
|
"channelLayout": "Channel Layout",
|
||||||
|
"maxFPS": "Max FPS",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"customGradients": "Custom Gradients",
|
||||||
|
"addCustomGradient": "Add Custom Gradient",
|
||||||
|
"gradientName": "Gradient Name",
|
||||||
|
"gradientNamePlaceholder": "Gradient Name",
|
||||||
|
"vertical": "Vertical",
|
||||||
|
"horizontal": "Horizontal",
|
||||||
|
"colorStops": "Color Stops",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"position": "Position",
|
||||||
|
"level": "Level",
|
||||||
|
"remove": "Remove",
|
||||||
|
"custom": "Custom",
|
||||||
|
"builtIn": "Built-in",
|
||||||
|
"colors": "Colors",
|
||||||
|
"colorMode": "Color Mode",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"gradientLeft": "Gradient Left",
|
||||||
|
"gradientRight": "Gradient Right",
|
||||||
|
"fft": "FFT",
|
||||||
|
"fftSize": "FFT Size",
|
||||||
|
"smoothing": "Smoothing",
|
||||||
|
"frequencyRangeAndScaling": "Frequency range and scaling",
|
||||||
|
"minimumFrequency": "Minimum Frequency",
|
||||||
|
"maximumFrequency": "Maximum Frequency",
|
||||||
|
"frequencyScale": "Frequency Scale",
|
||||||
|
"sensitivity": "Sensitivity",
|
||||||
|
"weightingFilter": "Weighting Filter",
|
||||||
|
"minimumDecibels": "Minimum Decibels",
|
||||||
|
"maximumDecibels": "Maximum Decibels",
|
||||||
|
"linearAmplitude": "Linear Amplitude",
|
||||||
|
"linearBoost": "Linear Boost",
|
||||||
|
"peakBehavior": "Peak Behavior",
|
||||||
|
"showPeaks": "Show Peaks",
|
||||||
|
"fadePeaks": "Fade Peaks",
|
||||||
|
"peakLine": "Peak Line",
|
||||||
|
"gravity": "Gravity",
|
||||||
|
"peakFadeTime": "Peak Fade Time (ms)",
|
||||||
|
"peakHoldTime": "Peak Hold Time (ms)",
|
||||||
|
"radialSpectrum": "Radial Spectrum",
|
||||||
|
"radial": "Radial",
|
||||||
|
"radialInvert": "Radial Invert",
|
||||||
|
"spinSpeed": "Spin Speed",
|
||||||
|
"radius": "Radius",
|
||||||
|
"reflexMirror": "Reflex Mirror",
|
||||||
|
"reflexFit": "Reflex Fit",
|
||||||
|
"reflexRatio": "Reflex Ratio",
|
||||||
|
"reflexAlpha": "Reflex Alpha",
|
||||||
|
"reflexBrightness": "Reflex Brightness",
|
||||||
|
"mirror": "Mirror",
|
||||||
|
"miscellaneousSettings": "Miscellaneous Settings",
|
||||||
|
"alphaBars": "Alpha Bars",
|
||||||
|
"ansiBands": "ANSI Bands",
|
||||||
|
"ledBars": "LED Bars",
|
||||||
|
"trueLeds": "True LEDs",
|
||||||
|
"lumiBars": "Lumi Bars",
|
||||||
|
"outlineBars": "Outline Bars",
|
||||||
|
"roundBars": "Round Bars",
|
||||||
|
"lowResolution": "Low Resolution",
|
||||||
|
"splitGradient": "Split Gradient",
|
||||||
|
"showFPS": "Show FPS",
|
||||||
|
"showScaleX": "Show Scale X",
|
||||||
|
"noteLabels": "Note Labels",
|
||||||
|
"showScaleY": "Show Scale Y",
|
||||||
|
"options": {
|
||||||
|
"mode": {
|
||||||
|
"bars": "[0] Bars",
|
||||||
|
"circle": "[1] Circle",
|
||||||
|
"wave": "[2] Wave",
|
||||||
|
"rainbow": "[3] Rainbow",
|
||||||
|
"rings": "[4] Rings",
|
||||||
|
"mirror": "[5] Mirror",
|
||||||
|
"line": "[6] Line",
|
||||||
|
"particles": "[7] Particles",
|
||||||
|
"fullOctave": "[8] Full octave / 10 bands",
|
||||||
|
"outlineBars": "[10] Outline bars"
|
||||||
|
},
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"barIndex": "Bar-Index",
|
||||||
|
"barLevel": "Bar-Level"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "Classic",
|
||||||
|
"prism": "Prism",
|
||||||
|
"rainbow": "Rainbow",
|
||||||
|
"steelblue": "Steelblue",
|
||||||
|
"orangered": "Orangered"
|
||||||
|
},
|
||||||
|
"channelLayout": {
|
||||||
|
"single": "Single",
|
||||||
|
"dualCombined": "Dual-Combined",
|
||||||
|
"dualHorizontal": "Dual-Horizontal",
|
||||||
|
"dualVertical": "Dual-Vertical"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"bark": "Bark",
|
||||||
|
"linear": "Linear",
|
||||||
|
"log": "Log",
|
||||||
|
"mel": "Mel"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "None",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"holdToShuffle": "Mantener para mezclar",
|
"holdToShuffle": "Mantener para mezclar",
|
||||||
"lyrics": "Letras",
|
"lyrics": "Letras",
|
||||||
"restoreQueueFromServer": "Restaurar cola del servidor",
|
"restoreQueueFromServer": "Restaurar cola del servidor",
|
||||||
"saveQueueToServer": "Guardar cola en el servidor"
|
"saveQueueToServer": "Guardar cola en el servidor",
|
||||||
|
"artistRadio": "Radio de artista",
|
||||||
|
"trackRadio": "Radio de pista"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
||||||
@@ -206,8 +208,6 @@
|
|||||||
"startMinimized_description": "inicia la aplicación en la bandeja del sistema",
|
"startMinimized_description": "inicia la aplicación en la bandeja del sistema",
|
||||||
"startMinimized": "iniciar minimizado",
|
"startMinimized": "iniciar minimizado",
|
||||||
"passwordStore": "contraseñas/almacenamiento secreto",
|
"passwordStore": "contraseñas/almacenamiento secreto",
|
||||||
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
|
|
||||||
"playerAlbumArtResolution": "resolución de la carátula del álbum del reproductor",
|
|
||||||
"homeConfiguration": "Configuración de la página de inicio",
|
"homeConfiguration": "Configuración de la página de inicio",
|
||||||
"mpvExtraParameters_help": "Uno por línea",
|
"mpvExtraParameters_help": "Uno por línea",
|
||||||
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
|
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
|
||||||
@@ -349,7 +349,11 @@
|
|||||||
"logLevel_optionInfo": "Información",
|
"logLevel_optionInfo": "Información",
|
||||||
"logLevel_optionWarn": "Advertencia",
|
"logLevel_optionWarn": "Advertencia",
|
||||||
"useThemeAccentColor": "Usar color de acentuación de tema",
|
"useThemeAccentColor": "Usar color de acentuación de tema",
|
||||||
"useThemeAccentColor_description": "Usa el color principal definido en el tema seleccionado en lugar del color de acentuación personalizado"
|
"useThemeAccentColor_description": "Usa el color principal definido en el tema seleccionado en lugar del color de acentuación personalizado",
|
||||||
|
"artistRadioCount_description": "Establece el número de canciones a buscar para la radio de artista y de pista",
|
||||||
|
"artistRadioCount": "Recuento de radio de artista/pista",
|
||||||
|
"imageResolution": "Resolución de imagen",
|
||||||
|
"imageResolution_description": "La resolución de las imágenes usadas en la aplicación. Usar un valor de 0 lo dejará de forma predeterminada a la resolución nativa de la imagen"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||||
@@ -385,7 +389,11 @@
|
|||||||
"moveUp": "Desplazar hacia arriba",
|
"moveUp": "Desplazar hacia arriba",
|
||||||
"moveDown": "Desplazar hacia abajo",
|
"moveDown": "Desplazar hacia abajo",
|
||||||
"createRadioStation": "Crear $t(entity.radioStation_one)",
|
"createRadioStation": "Crear $t(entity.radioStation_one)",
|
||||||
"deleteRadioStation": "Borrar $t(entity.radioStation_one)"
|
"deleteRadioStation": "Borrar $t(entity.radioStation_one)",
|
||||||
|
"openApplicationDirectory": "Abrir directorio de la aplicación",
|
||||||
|
"addOrRemoveFromSelection": "Añadir o quitar de la selección",
|
||||||
|
"selectRangeOfItems": "Seleccionar un intervalo de elementos",
|
||||||
|
"selectAll": "Seleccionar todo"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "hacia atrás",
|
"backward": "hacia atrás",
|
||||||
@@ -502,7 +510,9 @@
|
|||||||
"tableColumns": "Columnas de la tabla",
|
"tableColumns": "Columnas de la tabla",
|
||||||
"itemsMore": "{{count}} más",
|
"itemsMore": "{{count}} más",
|
||||||
"noFilters": "Ningún filtro configurado",
|
"noFilters": "Ningún filtro configurado",
|
||||||
"view": "Vista"
|
"view": "Vista",
|
||||||
|
"countSelected": "{{count}} seleccionados",
|
||||||
|
"retry": "Reintentar"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
||||||
@@ -530,7 +540,10 @@
|
|||||||
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe",
|
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe",
|
||||||
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto",
|
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto",
|
||||||
"saveQueueFailed": "Error al guardar la cola",
|
"saveQueueFailed": "Error al guardar la cola",
|
||||||
"multipleServerSaveQueueError": "La cola de reproducción tiene una o más canciones que no son del servidor actual. Esto no está soportado"
|
"multipleServerSaveQueueError": "La cola de reproducción tiene una o más canciones que no son del servidor actual. Esto no está soportado",
|
||||||
|
"settingsSyncError": "Se encontraron discrepancias entre las opciones del renderizador y el proceso principal. Reinicia la aplicación para aplicar los cambios",
|
||||||
|
"noNetwork": "Servidor no disponible",
|
||||||
|
"noNetworkDescription": "No se pudo conectar a este servidor"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "más reproducido",
|
"mostPlayed": "más reproducido",
|
||||||
@@ -700,7 +713,8 @@
|
|||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"sidebar": "Barra lateral",
|
"sidebar": "Barra lateral",
|
||||||
"playerFilters": "Filtros del reproductor",
|
"playerFilters": "Filtros del reproductor",
|
||||||
"logger": "Registrador"
|
"logger": "Registrador",
|
||||||
|
"lyricsDisplay": "Mostrar letras"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
@@ -866,6 +880,11 @@
|
|||||||
"input_homepageUrl": "URL de la página de inicio",
|
"input_homepageUrl": "URL de la página de inicio",
|
||||||
"input_name": "Nombre",
|
"input_name": "Nombre",
|
||||||
"input_streamUrl": "URL de la transmisión"
|
"input_streamUrl": "URL de la transmisión"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "Exportar letras",
|
||||||
|
"input_synced": "Exportar letras sincronizadas",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -1084,5 +1103,11 @@
|
|||||||
"notInTheLast": "no está en el último",
|
"notInTheLast": "no está en el último",
|
||||||
"startsWith": "empieza con",
|
"startsWith": "empieza con",
|
||||||
"matchesRegex": "coincide con expresión regular"
|
"matchesRegex": "coincide con expresión regular"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "min",
|
||||||
|
"secondShort": "seg",
|
||||||
|
"hourShort": "h",
|
||||||
|
"dayShort": "día"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,6 @@
|
|||||||
"playbackStyle_description": "aukeratu audio erreproduzitzailearentzat erabiliko den erreprodukzio estiloa",
|
"playbackStyle_description": "aukeratu audio erreproduzitzailearentzat erabiliko den erreprodukzio estiloa",
|
||||||
"playButtonBehavior": "erreprodukzio botoiaren portaera",
|
"playButtonBehavior": "erreprodukzio botoiaren portaera",
|
||||||
"playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean",
|
"playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean",
|
||||||
"playerAlbumArtResolution": "erreproduzitzailearen albumaren arte-azalaren erresoluzioa",
|
|
||||||
"gaplessAudio": "hutsune gabeko audioa",
|
"gaplessAudio": "hutsune gabeko audioa",
|
||||||
"gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako",
|
"gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako",
|
||||||
"passwordStore": "pasahitzak/biltegi sekretua",
|
"passwordStore": "pasahitzak/biltegi sekretua",
|
||||||
|
|||||||
@@ -474,7 +474,6 @@
|
|||||||
"replayGainClipping": "{{ReplayGain}} leikkaus",
|
"replayGainClipping": "{{ReplayGain}} leikkaus",
|
||||||
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
|
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
|
||||||
"replayGainFallback": "{{ReplayGain}} palautus",
|
"replayGainFallback": "{{ReplayGain}} palautus",
|
||||||
"playerAlbumArtResolution_description": "suurien kansikuvien resoluutio soittimen esikatselussa. suurempi tekee niistä terävempiä, mutta voi hidastaa latausta. oletuksena on 0, joka tarkoittaa automaattista",
|
|
||||||
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||||
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
|
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
|
||||||
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
|
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
|
||||||
@@ -490,7 +489,6 @@
|
|||||||
"sidebarConfiguration": "sivupalkin asetukset",
|
"sidebarConfiguration": "sivupalkin asetukset",
|
||||||
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
|
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
|
||||||
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
|
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
|
||||||
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
|
|
||||||
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
|
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
|
||||||
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
|
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
|
||||||
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
|
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
|
||||||
|
|||||||
+10
-11
@@ -14,7 +14,7 @@
|
|||||||
"shuffle": "lecture (mélangé)",
|
"shuffle": "lecture (mélangé)",
|
||||||
"playbackFetchNoResults": "aucun titre trouvé",
|
"playbackFetchNoResults": "aucun titre trouvé",
|
||||||
"playbackFetchInProgress": "chargement des titres…",
|
"playbackFetchInProgress": "chargement des titres…",
|
||||||
"addNext": "ajouter ensuite",
|
"addNext": "prochain",
|
||||||
"playbackSpeed": "vitesse de lecture",
|
"playbackSpeed": "vitesse de lecture",
|
||||||
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
|
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
|
||||||
"play": "lecture",
|
"play": "lecture",
|
||||||
@@ -24,15 +24,15 @@
|
|||||||
"queue_moveToTop": "déplacer la sélection vers le bas",
|
"queue_moveToTop": "déplacer la sélection vers le bas",
|
||||||
"queue_moveToBottom": "déplacer la sélection vers le haut",
|
"queue_moveToBottom": "déplacer la sélection vers le haut",
|
||||||
"shuffle_off": "aléatoire désactivée",
|
"shuffle_off": "aléatoire désactivée",
|
||||||
"addLast": "ajouter en dernier",
|
"addLast": "dernier",
|
||||||
"mute": "muet",
|
"mute": "muet",
|
||||||
"skip_forward": "avancer",
|
"skip_forward": "avancer",
|
||||||
"pause": "pause",
|
"pause": "pause",
|
||||||
"unfavorite": "retirer des favoris",
|
"unfavorite": "retirer des favoris",
|
||||||
"playSimilarSongs": "jouer des titres similaires",
|
"playSimilarSongs": "jouer des titres similaires",
|
||||||
"viewQueue": "voir la file d'attente",
|
"viewQueue": "voir la file d'attente",
|
||||||
"addLastShuffled": "ajouter en dernier (mélangé)",
|
"addLastShuffled": "dernier (mélangé)",
|
||||||
"addNextShuffled": "ajouter ensuite (mélangé)",
|
"addNextShuffled": "prochain (mélangé)",
|
||||||
"queueType": "type de file d'attente",
|
"queueType": "type de file d'attente",
|
||||||
"queueType_default": "défaut",
|
"queueType_default": "défaut",
|
||||||
"queueType_priority": "priorité",
|
"queueType_priority": "priorité",
|
||||||
@@ -223,7 +223,8 @@
|
|||||||
"badValue": "option {{value}} invalide. Cette valeur n'existe plus",
|
"badValue": "option {{value}} invalide. Cette valeur n'existe plus",
|
||||||
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet",
|
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet",
|
||||||
"multipleServerSaveQueueError": "la file d'attente de lecture contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
|
"multipleServerSaveQueueError": "la file d'attente de lecture contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
|
||||||
"saveQueueFailed": "échec de l'enregistrement de la file d'attente"
|
"saveQueueFailed": "échec de l'enregistrement de la file d'attente",
|
||||||
|
"settingsSyncError": "des incohérences ont été détectées entre les paramètres du moteur de rendu et ceux du processus principal. redémarrez l'application pour appliquer les modifications"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "plus joués",
|
"mostPlayed": "plus joués",
|
||||||
@@ -633,9 +634,7 @@
|
|||||||
"imageAspectRatio_description": "si cette option est activée, les pochettes d'album seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide",
|
"imageAspectRatio_description": "si cette option est activée, les pochettes d'album seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide",
|
||||||
"mpvExtraParameters_help": "un par ligne",
|
"mpvExtraParameters_help": "un par ligne",
|
||||||
"passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe",
|
"passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe",
|
||||||
"playerAlbumArtResolution": "résolution de la pochette d'album du lecteur",
|
|
||||||
"passwordStore": "mots de passe",
|
"passwordStore": "mots de passe",
|
||||||
"playerAlbumArtResolution_description": "résolution pour l'aperçu de la pochette d'album agrandie du lecteur. plus grand le rend plus net, mais peut ralentir le chargement. la valeur par défaut est 0 (automatique)",
|
|
||||||
"homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre",
|
"homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre",
|
||||||
"startMinimized": "démarrer l'application en mode réduit",
|
"startMinimized": "démarrer l'application en mode réduit",
|
||||||
"transcode_description": "permet le transcodage vers différents formats",
|
"transcode_description": "permet le transcodage vers différents formats",
|
||||||
@@ -925,11 +924,11 @@
|
|||||||
"song_many": "titres",
|
"song_many": "titres",
|
||||||
"song_other": "titres",
|
"song_other": "titres",
|
||||||
"radioStation_one": "station radio",
|
"radioStation_one": "station radio",
|
||||||
"radioStation_many": "stations radios",
|
"radioStation_many": "stations radio",
|
||||||
"radioStation_other": "",
|
"radioStation_other": "stations radio",
|
||||||
"radioStationWithCount_one": "{{count}} station radio",
|
"radioStationWithCount_one": "{{count}} station radio",
|
||||||
"radioStationWithCount_many": "{{count}} stations radios",
|
"radioStationWithCount_many": "{{count}} stations radio",
|
||||||
"radioStationWithCount_other": ""
|
"radioStationWithCount_other": "{{count}} stations radio"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
+50
-13
@@ -31,7 +31,13 @@
|
|||||||
"moveUp": "ugrás fel",
|
"moveUp": "ugrás fel",
|
||||||
"moveDown": "ugrás le",
|
"moveDown": "ugrás le",
|
||||||
"holdToMoveToTop": "hosszan nyomva felülre mozgat",
|
"holdToMoveToTop": "hosszan nyomva felülre mozgat",
|
||||||
"holdToMoveToBottom": "hosszan nyomva lejjebb mozgat"
|
"holdToMoveToBottom": "hosszan nyomva lejjebb mozgat",
|
||||||
|
"selectAll": "összes kijelölése",
|
||||||
|
"deleteRadioStation": "$t(entity.radioStation_one) törlése",
|
||||||
|
"createRadioStation": "$t(entity.radioStation_one) létrehozása",
|
||||||
|
"openApplicationDirectory": "app könyvtár megnyitása",
|
||||||
|
"addOrRemoveFromSelection": "hozzáadás vagy eltávolítás a kiválasztásból",
|
||||||
|
"selectRangeOfItems": "válaszd ki a tartományt"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"collapse": "összecsukás",
|
"collapse": "összecsukás",
|
||||||
@@ -145,7 +151,9 @@
|
|||||||
"tableColumns": "táblázat oszlopok",
|
"tableColumns": "táblázat oszlopok",
|
||||||
"itemsMore": "{{count}} még több",
|
"itemsMore": "{{count}} még több",
|
||||||
"view": "nézet",
|
"view": "nézet",
|
||||||
"noFilters": "nincs konfigurált szűrő"
|
"noFilters": "nincs konfigurált szűrő",
|
||||||
|
"countSelected": "{{count}} kiválasztott",
|
||||||
|
"retry": "újra"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"albumArtist_one": "Zenész",
|
"albumArtist_one": "Zenész",
|
||||||
@@ -184,7 +192,9 @@
|
|||||||
"trackWithCount_one": "{{count}} sáv",
|
"trackWithCount_one": "{{count}} sáv",
|
||||||
"trackWithCount_other": "{{count}} sávok",
|
"trackWithCount_other": "{{count}} sávok",
|
||||||
"radioStation_one": "rádió állomás",
|
"radioStation_one": "rádió állomás",
|
||||||
"radioStation_other": "rádió állomások"
|
"radioStation_other": "rádió állomások",
|
||||||
|
"radioStationWithCount_one": "{{count}} rádióállomás",
|
||||||
|
"radioStationWithCount_other": "{{count}} rádióállomások"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"apiRouteError": "a kérést nem sikerült célba juttatni",
|
"apiRouteError": "a kérést nem sikerült célba juttatni",
|
||||||
@@ -210,7 +220,12 @@
|
|||||||
"serverRequired": "szerver szükséges",
|
"serverRequired": "szerver szükséges",
|
||||||
"serverNotSelectedError": "nincs szerver kiválasztva",
|
"serverNotSelectedError": "nincs szerver kiválasztva",
|
||||||
"notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan",
|
"notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan",
|
||||||
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik"
|
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik",
|
||||||
|
"noNetwork": "Szerver nem elérhető",
|
||||||
|
"noNetworkDescription": "Nem tudok csatlakozni a szerverhez",
|
||||||
|
"saveQueueFailed": "műsorlista mentése sikertelen",
|
||||||
|
"settingsSyncError": "Eltéréseket találtam a leképző és a fő folyamat beállításai között. Indítsd újra az alkalmazást",
|
||||||
|
"multipleServerSaveQueueError": "a műsorlistában egy vagy több olyan dal található, amely nem az aktuális szerverről származik. Ez nem támogatott"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"albumCount": "$t(entity.album_other) darab",
|
"albumCount": "$t(entity.album_other) darab",
|
||||||
@@ -345,6 +360,16 @@
|
|||||||
"input_played": "csak szűrt zenék",
|
"input_played": "csak szűrt zenék",
|
||||||
"input_played_optionUnplayed": "Csak a még nem lejátszottak",
|
"input_played_optionUnplayed": "Csak a még nem lejátszottak",
|
||||||
"input_played_optionPlayed": "Csak a játszottak számok"
|
"input_played_optionPlayed": "Csak a játszottak számok"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"success": "rádió állomás sikeresen létrehozva",
|
||||||
|
"title": "rádió állomás létrehozása",
|
||||||
|
"input_homepageUrl": "oldal url",
|
||||||
|
"input_name": "név",
|
||||||
|
"input_streamUrl": "stream url"
|
||||||
|
},
|
||||||
|
"saveQueue": {
|
||||||
|
"success": "mentett lejátszási műsorlista a szerverre"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
@@ -525,7 +550,8 @@
|
|||||||
"settings": "$t(common.setting_other)",
|
"settings": "$t(common.setting_other)",
|
||||||
"shared": "megosztott $t(entity.playlist_other)",
|
"shared": "megosztott $t(entity.playlist_other)",
|
||||||
"tracks": "$t(entity.track_other)",
|
"tracks": "$t(entity.track_other)",
|
||||||
"favorites": "$t(entity.favorite_other)"
|
"favorites": "$t(entity.favorite_other)",
|
||||||
|
"radio": "$t(entity.radioStation_other)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"artistTracks": "dalok tőle {{artist}}",
|
"artistTracks": "dalok tőle {{artist}}",
|
||||||
@@ -537,11 +563,14 @@
|
|||||||
},
|
},
|
||||||
"folderList": {
|
"folderList": {
|
||||||
"title": "$t(entity.folder_other)"
|
"title": "$t(entity.folder_other)"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "rádió állomások"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"addLast": "utoljára hozzáadva",
|
"addLast": "utolsónak",
|
||||||
"addNext": "következő hozzáadása",
|
"addNext": "következő",
|
||||||
"favorite": "kedvenc",
|
"favorite": "kedvenc",
|
||||||
"mute": "némítás",
|
"mute": "némítás",
|
||||||
"muted": "némítva",
|
"muted": "némítva",
|
||||||
@@ -571,13 +600,15 @@
|
|||||||
"pause": "szünet",
|
"pause": "szünet",
|
||||||
"viewQueue": "műsorlista megtekintése",
|
"viewQueue": "műsorlista megtekintése",
|
||||||
"shuffle_off": "kevert lejátszás ki",
|
"shuffle_off": "kevert lejátszás ki",
|
||||||
"addLastShuffled": "Hozzáadás a végére (keverve)",
|
"addLastShuffled": "végére (keverve)",
|
||||||
"addNextShuffled": "Hozzáadás következőnek (keverve)",
|
"addNextShuffled": "következő (keverve)",
|
||||||
"queueType": "lekérdezés típus",
|
"queueType": "lekérdezés típus",
|
||||||
"queueType_default": "alapértelmezett",
|
"queueType_default": "alapértelmezett",
|
||||||
"queueType_priority": "prioritás",
|
"queueType_priority": "prioritás",
|
||||||
"holdToShuffle": "tartsd lenyomva a keveréshez",
|
"holdToShuffle": "tartsd lenyomva a keveréshez",
|
||||||
"lyrics": "dalszöveg"
|
"lyrics": "dalszöveg",
|
||||||
|
"saveQueueToServer": "műsorlista mentése a szerverre",
|
||||||
|
"restoreQueueFromServer": "műsorlista visszaállítása a szerverről"
|
||||||
},
|
},
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
"primary": {
|
"primary": {
|
||||||
@@ -766,7 +797,6 @@
|
|||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playButtonBehavior": "lejátszás gomb viselkedése",
|
"playButtonBehavior": "lejátszás gomb viselkedése",
|
||||||
"playerAlbumArtResolution_description": "A nagy lejátszó albumborító-előnézetének felbontása. A nagyobb érték élesebb képet ad, de lassíthatja a betöltést. Alapértelmezés: 0, ami az automatikus módot jelenti",
|
|
||||||
"minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
|
"minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
|
||||||
"minimumScrobblePercentage": "Minimális Scrobble arány (százalék)",
|
"minimumScrobblePercentage": "Minimális Scrobble arány (százalék)",
|
||||||
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
|
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
|
||||||
@@ -780,7 +810,6 @@
|
|||||||
"notify": "bekapcsolja a dal értesítéseket",
|
"notify": "bekapcsolja a dal értesítéseket",
|
||||||
"notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor",
|
"notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor",
|
||||||
"playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust",
|
"playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust",
|
||||||
"playerAlbumArtResolution": "lejátszó albumborító felbontás",
|
|
||||||
"playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását",
|
"playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását",
|
||||||
"playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás",
|
"playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás",
|
||||||
"preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben",
|
"preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben",
|
||||||
@@ -908,7 +937,9 @@
|
|||||||
"playerFilters_description": "a következő kritériumok alapján kihagyja a dalokat a műsorlistából",
|
"playerFilters_description": "a következő kritériumok alapján kihagyja a dalokat a műsorlistából",
|
||||||
"playerbarSlider_description": "a hullámforma nem ajánlott lassú vagy korlátozott internetkapcsolat esetén",
|
"playerbarSlider_description": "a hullámforma nem ajánlott lassú vagy korlátozott internetkapcsolat esetén",
|
||||||
"audioFadeOnStatusChange": "audio behúzás állapotváltozáskor",
|
"audioFadeOnStatusChange": "audio behúzás állapotváltozáskor",
|
||||||
"audioFadeOnStatusChange_description": "lehetővé teszi a lehúzást és a behúzást, amikor a lejátszás/szünet állapot megváltozik"
|
"audioFadeOnStatusChange_description": "lehetővé teszi a lehúzást és a behúzást, amikor a lejátszás/szünet állapot megváltozik",
|
||||||
|
"useThemeAccentColor": "használd a téma kiemelő színét",
|
||||||
|
"useThemeAccentColor_description": "a kiválasztott témában meghatározott alapszínt használja az egyéni kiemelő szín helyett"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1038,5 +1069,11 @@
|
|||||||
"matchesRegex": "illeszkedik a regexre",
|
"matchesRegex": "illeszkedik a regexre",
|
||||||
"is": "van",
|
"is": "van",
|
||||||
"isNot": "nincs"
|
"isNot": "nincs"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "perc",
|
||||||
|
"secondShort": "mp",
|
||||||
|
"hourShort": "óra",
|
||||||
|
"dayShort": "nap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,8 +579,6 @@
|
|||||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "resolusi sampul album pemutar",
|
|
||||||
"playerAlbumArtResolution_description": "resolusi untuk pratinjau sampul album pemutar besar. semakin besar akan membuatnya lebih tajam, tetapi dapat memperlambat pemuatan. Nilai default adalah 0, yang berarti otomatis",
|
|
||||||
"playerbarOpenDrawer": "Buka pemutar ke layar penuh",
|
"playerbarOpenDrawer": "Buka pemutar ke layar penuh",
|
||||||
"playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
|
"playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
|
||||||
"remotePassword": "kata sandi kontrol jarak jauh server",
|
"remotePassword": "kata sandi kontrol jarak jauh server",
|
||||||
|
|||||||
@@ -355,8 +355,6 @@
|
|||||||
"passwordStore": "Archivio di password/segreti",
|
"passwordStore": "Archivio di password/segreti",
|
||||||
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali",
|
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "risoluzione della copertina nel lettore",
|
|
||||||
"playerAlbumArtResolution_description": "la risoluzione dell’anteprima della copertina nel lettore in formato grande. valori più alti la rendono più nitida, ma possono rallentare il caricamento. Il valore predefinito è 0, che indica la modalità automatica",
|
|
||||||
"sidePlayQueueStyle_optionAttached": "fissata",
|
"sidePlayQueueStyle_optionAttached": "fissata",
|
||||||
"sidePlayQueueStyle_optionDetached": "sganciata",
|
"sidePlayQueueStyle_optionDetached": "sganciata",
|
||||||
"startMinimized": "avvia minimizzato",
|
"startMinimized": "avvia minimizzato",
|
||||||
|
|||||||
@@ -203,7 +203,6 @@
|
|||||||
"volumeWidth_description": "音量スライダーの幅",
|
"volumeWidth_description": "音量スライダーの幅",
|
||||||
"volumeWidth": "音量スライダーの幅",
|
"volumeWidth": "音量スライダーの幅",
|
||||||
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
|
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
|
||||||
"playerAlbumArtResolution_description": "大画面プレーヤーのアルバムアートプレビューの解像度。解像度が高いほど鮮明になりますが、読み込みが遅くなる可能性があります。デフォルトは 0 (自動設定) です",
|
|
||||||
"mpvExtraParameters_help": "1 行に 1 つずつ",
|
"mpvExtraParameters_help": "1 行に 1 つずつ",
|
||||||
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
|
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
|
||||||
"musicbrainz": "MusicBrainz リンクを表示する",
|
"musicbrainz": "MusicBrainz リンクを表示する",
|
||||||
@@ -211,7 +210,6 @@
|
|||||||
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
|
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
|
||||||
"passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
|
"passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
|
||||||
"passwordStore": "パスワード/シークレットストア",
|
"passwordStore": "パスワード/シークレットストア",
|
||||||
"playerAlbumArtResolution": "プレーヤーのアルバムアートの解像度",
|
|
||||||
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
|
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
|
||||||
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
|
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
|
||||||
"preferLocalLyrics": "ローカル歌詞を優先する",
|
"preferLocalLyrics": "ローカル歌詞を優先する",
|
||||||
|
|||||||
@@ -21,7 +21,23 @@
|
|||||||
},
|
},
|
||||||
"viewPlaylists": "$t(entity.playlist_other) 보기",
|
"viewPlaylists": "$t(entity.playlist_other) 보기",
|
||||||
"setRating": "평점 지정",
|
"setRating": "평점 지정",
|
||||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기"
|
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기",
|
||||||
|
"addOrRemoveFromSelection": "선택항목에서 추가 또는 제거",
|
||||||
|
"selectRangeOfItems": "항목의 범위 선택",
|
||||||
|
"createRadioStation": "$t(entity.radioStation_one) 생성",
|
||||||
|
"deleteRadioStation": "$t(entity.radioStation_one) 삭제",
|
||||||
|
"selectAll": "전부 선택",
|
||||||
|
"downloadStarted": "{{count}}개 항목 다운로드 시작했습니다",
|
||||||
|
"moveUp": "위로 옮기기",
|
||||||
|
"moveDown": "아래로 옮기기",
|
||||||
|
"holdToMoveToTop": "맨 위로 옮기기 위해 끌기",
|
||||||
|
"holdToMoveToBottom": "맨 아래로 옮기기 위해 끌기",
|
||||||
|
"moveItems": "항목 옮기기",
|
||||||
|
"shuffle": "섞기",
|
||||||
|
"shuffleAll": "모두 섞기",
|
||||||
|
"shuffleSelected": "선택항목 섞기",
|
||||||
|
"viewMore": "더 보기",
|
||||||
|
"openApplicationDirectory": "앱 디렉토리 열기"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"translation": "번역",
|
"translation": "번역",
|
||||||
@@ -122,7 +138,18 @@
|
|||||||
"recordLabel": "레이블",
|
"recordLabel": "레이블",
|
||||||
"releaseType": "발매형태",
|
"releaseType": "발매형태",
|
||||||
"explicit": "성인컨텐츠",
|
"explicit": "성인컨텐츠",
|
||||||
"clean": "클린"
|
"clean": "클린",
|
||||||
|
"countSelected": "{{count}}개 선택됨",
|
||||||
|
"doNotShowAgain": "다시 보지 않기",
|
||||||
|
"view": "보기",
|
||||||
|
"externalLinks": "외부 링크",
|
||||||
|
"faster": "빠르게",
|
||||||
|
"noFilters": "필터 미설정",
|
||||||
|
"slower": "천천히",
|
||||||
|
"sort": "정렬",
|
||||||
|
"gridRows": "행 그리드",
|
||||||
|
"tableColumns": "테이블 열",
|
||||||
|
"itemsMore": "{{count}}개 더"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"albumWithCount_other": "{{count}} 앨범",
|
"albumWithCount_other": "{{count}} 앨범",
|
||||||
@@ -142,7 +169,9 @@
|
|||||||
"play_other": "{{count}} 재생",
|
"play_other": "{{count}} 재생",
|
||||||
"playlistWithCount_other": "{{count}} 재생목록",
|
"playlistWithCount_other": "{{count}} 재생목록",
|
||||||
"smartPlaylist": "스마트 $t(entity.playlist_one)",
|
"smartPlaylist": "스마트 $t(entity.playlist_one)",
|
||||||
"track_other": "트랙"
|
"track_other": "트랙",
|
||||||
|
"radioStation_other": "라디오 방송국",
|
||||||
|
"radioStationWithCount_other": "{{count}}개 라디오 방송국"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
|
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
|
||||||
|
|||||||
@@ -27,7 +27,17 @@
|
|||||||
"shuffle": "shuffle",
|
"shuffle": "shuffle",
|
||||||
"shuffleAll": "shuffle alles",
|
"shuffleAll": "shuffle alles",
|
||||||
"shuffleSelected": "shuffle geselecteerde",
|
"shuffleSelected": "shuffle geselecteerde",
|
||||||
"viewMore": "bekijk meer"
|
"viewMore": "bekijk meer",
|
||||||
|
"addOrRemoveFromSelection": "toevoegen of verwijderen van selectie",
|
||||||
|
"selectRangeOfItems": "selecteer een reeks van nummers",
|
||||||
|
"createRadioStation": "maak $t(entity.radioStation_one)",
|
||||||
|
"deleteRadioStation": "verwijder $t(entity.radioStation_one)",
|
||||||
|
"selectAll": "selecteer alles",
|
||||||
|
"moveUp": "beweeg naar boven",
|
||||||
|
"moveDown": "beweeg naar beneden",
|
||||||
|
"holdToMoveToTop": "ingedrukt houden om naar boven te verplaatsen",
|
||||||
|
"holdToMoveToBottom": "ingedrukt houden om naar beneden te verplaatsen",
|
||||||
|
"openApplicationDirectory": "applicatiefolder openen"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "achteruit",
|
"backward": "achteruit",
|
||||||
@@ -139,7 +149,10 @@
|
|||||||
"clean": "schoon",
|
"clean": "schoon",
|
||||||
"gridRows": "rasterrijen",
|
"gridRows": "rasterrijen",
|
||||||
"tableColumns": "tabelkolommen",
|
"tableColumns": "tabelkolommen",
|
||||||
"itemsMore": "{{count}} meer"
|
"itemsMore": "{{count}} meer",
|
||||||
|
"countSelected": "{{count}} geselecteerd",
|
||||||
|
"view": "bekijken",
|
||||||
|
"noFilters": "geen filters ingesteld"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"rating": "rating",
|
"rating": "rating",
|
||||||
@@ -427,7 +440,11 @@
|
|||||||
"song_one": "lied",
|
"song_one": "lied",
|
||||||
"song_other": "liedjes",
|
"song_other": "liedjes",
|
||||||
"play_one": "{{count}} keer afgespeeld",
|
"play_one": "{{count}} keer afgespeeld",
|
||||||
"play_other": "{{count}} keren afgespeeld"
|
"play_other": "{{count}} keren afgespeeld",
|
||||||
|
"radioStation_one": "radiostation",
|
||||||
|
"radioStation_other": "radiostations",
|
||||||
|
"radioStationWithCount_one": "{{count}} radiostation",
|
||||||
|
"radioStationWithCount_other": "{{count}} radiostations"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
|
|||||||
+185
-9
@@ -33,7 +33,11 @@
|
|||||||
"holdToMoveToTop": "przytrzymaj aby, przesunąć na górę",
|
"holdToMoveToTop": "przytrzymaj aby, przesunąć na górę",
|
||||||
"holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół",
|
"holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół",
|
||||||
"createRadioStation": "utwórz $t(entity.radioStation_one)",
|
"createRadioStation": "utwórz $t(entity.radioStation_one)",
|
||||||
"deleteRadioStation": "usuń $t(entity.radioStation_one)"
|
"deleteRadioStation": "usuń $t(entity.radioStation_one)",
|
||||||
|
"addOrRemoveFromSelection": "dodaj lub usuń z wyboru",
|
||||||
|
"selectRangeOfItems": "wybierz zakres elementów",
|
||||||
|
"selectAll": "wybierz wszystkie",
|
||||||
|
"openApplicationDirectory": "otwórz katalog aplikacji"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"increase": "zwiększ",
|
"increase": "zwiększ",
|
||||||
@@ -150,7 +154,9 @@
|
|||||||
"tableColumns": "tabela kolumn",
|
"tableColumns": "tabela kolumn",
|
||||||
"itemsMore": "{{count}} więcej",
|
"itemsMore": "{{count}} więcej",
|
||||||
"noFilters": "nie skonfigurowano filtrów",
|
"noFilters": "nie skonfigurowano filtrów",
|
||||||
"view": "wyświetl"
|
"view": "wyświetl",
|
||||||
|
"countSelected": "wybrano {{count}}",
|
||||||
|
"retry": "spróbuj ponownie"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "gatunek",
|
"genre_one": "gatunek",
|
||||||
@@ -238,7 +244,10 @@
|
|||||||
"badValue": "niewłaściwa opcja \"{{value}}\". ta wartość już nie istnieje",
|
"badValue": "niewłaściwa opcja \"{{value}}\". ta wartość już nie istnieje",
|
||||||
"notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu",
|
"notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu",
|
||||||
"multipleServerSaveQueueError": "kolejka odtwarzania ma jedną lub więcej piosenek które nie pochodzą z aktualnego serwera. to nie jest wspierane",
|
"multipleServerSaveQueueError": "kolejka odtwarzania ma jedną lub więcej piosenek które nie pochodzą z aktualnego serwera. to nie jest wspierane",
|
||||||
"saveQueueFailed": "nie udało się zapisać kolejki"
|
"saveQueueFailed": "nie udało się zapisać kolejki",
|
||||||
|
"settingsSyncError": "zostały znalezione różnice pomiędzy ustawieniami w rendererze a głównym procesem. uruchom aplikację ponownie aby, zastosować zmiany",
|
||||||
|
"noNetwork": "serwer niedostępny",
|
||||||
|
"noNetworkDescription": "nie udało się połączyć z tym serwerem"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "najczęściej odtwarzane",
|
"mostPlayed": "najczęściej odtwarzane",
|
||||||
@@ -383,6 +392,11 @@
|
|||||||
"input_homepageUrl": "url strony głównej",
|
"input_homepageUrl": "url strony głównej",
|
||||||
"input_name": "nazwa",
|
"input_name": "nazwa",
|
||||||
"input_streamUrl": "url strumienia"
|
"input_streamUrl": "url strumienia"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "eksportuj tekst",
|
||||||
|
"input_synced": "eksportuj zsynchronizowany tekst",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -521,7 +535,8 @@
|
|||||||
"transcoding": "transkodowanie",
|
"transcoding": "transkodowanie",
|
||||||
"discord": "discord",
|
"discord": "discord",
|
||||||
"playerFilters": "filtry odtwarzacza",
|
"playerFilters": "filtry odtwarzacza",
|
||||||
"logger": "logger"
|
"logger": "logger",
|
||||||
|
"lyricsDisplay": "wyświetlanie tekstu"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
@@ -548,7 +563,9 @@
|
|||||||
"viewDiscography": "przeglądaj dyskografię",
|
"viewDiscography": "przeglądaj dyskografię",
|
||||||
"relatedArtists": "powiązane z $t(entity.artist_other)",
|
"relatedArtists": "powiązane z $t(entity.artist_other)",
|
||||||
"appearsOn": "pojawia się na",
|
"appearsOn": "pojawia się na",
|
||||||
"viewAllTracks": "zobacz wszystko $t(entity.track_other)"
|
"viewAllTracks": "zobacz wszystko $t(entity.track_other)",
|
||||||
|
"groupingTypeAll": "wszystkie typy wydań",
|
||||||
|
"groupingTypePrimary": "główne typy wydań"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "kopiuj ścieżkę do schowka",
|
"copyPath": "kopiuj ścieżkę do schowka",
|
||||||
@@ -616,7 +633,9 @@
|
|||||||
"holdToShuffle": "przytrzymaj aby odtwarzać losowo",
|
"holdToShuffle": "przytrzymaj aby odtwarzać losowo",
|
||||||
"lyrics": "tekst",
|
"lyrics": "tekst",
|
||||||
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
||||||
"saveQueueToServer": "zapisz kolejkę na serwerze"
|
"saveQueueToServer": "zapisz kolejkę na serwerze",
|
||||||
|
"artistRadio": "radio wykonawcy",
|
||||||
|
"trackRadio": "radio utworu"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
||||||
@@ -779,12 +798,10 @@
|
|||||||
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie playlist, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie playlist, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
||||||
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
||||||
"clearCache": "wyczyść pamięć podręczną przeglądarki",
|
"clearCache": "wyczyść pamięć podręczną przeglądarki",
|
||||||
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
|
|
||||||
"externalLinks": "pokaż zewnętrzne linki",
|
"externalLinks": "pokaż zewnętrzne linki",
|
||||||
"mpvExtraParameters_help": "po jednym na linię",
|
"mpvExtraParameters_help": "po jednym na linię",
|
||||||
"passwordStore": "hasła",
|
"passwordStore": "hasła",
|
||||||
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł",
|
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł",
|
||||||
"playerAlbumArtResolution_description": "rozdzielczość podglądu okładki albumu w dużym odtwarzaczu. większa sprawia, że wygląda bardziej wyraziście, ale może spowolnić ładowanie. domyślnie 0, czyli auto",
|
|
||||||
"startMinimized": "uruchom zminimalizowany",
|
"startMinimized": "uruchom zminimalizowany",
|
||||||
"startMinimized_description": "uruchom aplikację w zasobniku systemowym",
|
"startMinimized_description": "uruchom aplikację w zasobniku systemowym",
|
||||||
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
|
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
|
||||||
@@ -926,7 +943,18 @@
|
|||||||
"logLevel_optionInfo": "info",
|
"logLevel_optionInfo": "info",
|
||||||
"logLevel_optionWarn": "ostrzeżenia",
|
"logLevel_optionWarn": "ostrzeżenia",
|
||||||
"useThemeAccentColor": "używaj koloru akcentu motywu",
|
"useThemeAccentColor": "używaj koloru akcentu motywu",
|
||||||
"useThemeAccentColor_description": "używaj głównego koloru ustawionego w wybranym motywie zamiast niestandardowego koloru akcentu"
|
"useThemeAccentColor_description": "używaj głównego koloru ustawionego w wybranym motywie zamiast niestandardowego koloru akcentu",
|
||||||
|
"artistRadioCount_description": "ustawia liczbę piosenek do załadowania dla radia wykonawcy i radia utworu",
|
||||||
|
"artistRadioCount": "liczba radio wykonawców/utworów",
|
||||||
|
"imageResolution": "rozdzielczość obrazu",
|
||||||
|
"imageResolution_description": "rozdzielczość dla obrazów używanych w programie. użycie wartości 0 ustawi rozdzielczość na natywną",
|
||||||
|
"imageResolution_optionTable": "tabela",
|
||||||
|
"imageResolution_optionItemCard": "karta elementu",
|
||||||
|
"imageResolution_optionSidebar": "pasek boczny",
|
||||||
|
"imageResolution_optionHeader": "nagłówek",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "odtwarzacz pełnoekranowy",
|
||||||
|
"combinedLyricsAndVisualizer_description": "połącz tekst i wizualizacje w tym samym panelu",
|
||||||
|
"combinedLyricsAndVisualizer": "połącz tekst i wizualizacje w pasku bocznym odtwarzacza"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1084,5 +1112,153 @@
|
|||||||
"notInPlaylist": "nie jest w",
|
"notInPlaylist": "nie jest w",
|
||||||
"notInTheLast": "nie jest w ostatnim",
|
"notInTheLast": "nie jest w ostatnim",
|
||||||
"startsWith": "zaczyna się od"
|
"startsWith": "zaczyna się od"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "min",
|
||||||
|
"secondShort": "sek",
|
||||||
|
"hourShort": "godz",
|
||||||
|
"dayShort": "dzień"
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"visualizerType": "Typ Wizualizacji",
|
||||||
|
"cycleTime": "Czas cyklu (w sekundach)",
|
||||||
|
"copyConfiguration": "Kopiuj Konfigurację",
|
||||||
|
"pasteConfiguration": "Wklej Konfigurację",
|
||||||
|
"pasteConfigurationPlaceholder": "Wklej konfigurację JSON tutaj...",
|
||||||
|
"pasteFromClipboard": "Wklej z schowka",
|
||||||
|
"applyConfiguration": "Zastosuj Konfigurację",
|
||||||
|
"configCopied": "Konfiguracja skopiowana do schowka",
|
||||||
|
"configCopyFailed": "Nie udało się skopiować konfiguracji",
|
||||||
|
"configPasted": "Konfiguracja zastosowana pomyślnie",
|
||||||
|
"configPasteFailed": "Nie udało się zastosować konfiguracji. Sprawdź jej format.",
|
||||||
|
"configPasteReadFailed": "Nie udało się odczytać z schowka",
|
||||||
|
"cyclePresets": "Cykl Ustawień",
|
||||||
|
"includeAllPresets": "Uwzględnij wszystkie Ustawienia",
|
||||||
|
"ignoredPresets": "Ignorowane Ustawienia",
|
||||||
|
"selectedPresets": "Wybrane Ustawienia",
|
||||||
|
"randomizeNextPreset": "Losuj Następne Ustawienie",
|
||||||
|
"blendTime": "Czas Mieszania",
|
||||||
|
"presets": "Ustawienia",
|
||||||
|
"selectPreset": "Wybierz Ustawienie",
|
||||||
|
"applyPreset": "Zastosuj Ustawienie",
|
||||||
|
"saveAsPreset": "Zapisz jako Ustawienie",
|
||||||
|
"updatePreset": "Uaktualnij Ustawienie",
|
||||||
|
"presetName": "Nazwa Ustawienia",
|
||||||
|
"presetNamePlaceholder": "Wpisz nazwę ustawienia",
|
||||||
|
"general": "Ogólne",
|
||||||
|
"mode": "Tryb",
|
||||||
|
"mode1To8": "Tryb 1 - 8",
|
||||||
|
"mode10": "Tryb 10",
|
||||||
|
"barSpace": "Odstęp Pasków",
|
||||||
|
"lineWidth": "Szerokość Linii",
|
||||||
|
"fillAlpha": "Wypełnij Alpha",
|
||||||
|
"channelLayout": "Układ Kanałów",
|
||||||
|
"maxFPS": "Maks FPS",
|
||||||
|
"opacity": "Nieprzezroczystość",
|
||||||
|
"customGradients": "Niestandardowe Gradienty",
|
||||||
|
"addCustomGradient": "Dodaj Niestandardowy Gradient",
|
||||||
|
"gradientName": "Nazwa Gradientu",
|
||||||
|
"gradientNamePlaceholder": "Nazwa Gradientu",
|
||||||
|
"vertical": "Pionowy",
|
||||||
|
"horizontal": "Poziomy",
|
||||||
|
"colorStops": "Kroki Kolorów",
|
||||||
|
"addColor": "Dodaj Kolor",
|
||||||
|
"position": "Pozycja",
|
||||||
|
"level": "Poziom",
|
||||||
|
"remove": "Usuń",
|
||||||
|
"custom": "Niestandardowy",
|
||||||
|
"builtIn": "Wbudowany",
|
||||||
|
"colors": "Kolory",
|
||||||
|
"colorMode": "Tryb Koloru",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"gradientLeft": "Lewa Gradientu",
|
||||||
|
"gradientRight": "Prawa Gradientu",
|
||||||
|
"fft": "FFT",
|
||||||
|
"fftSize": "Rozmiar FFT",
|
||||||
|
"smoothing": "Wygładzanie",
|
||||||
|
"frequencyRangeAndScaling": "Zakres częstotliwości i skalowanie",
|
||||||
|
"minimumFrequency": "Minimalna Częstotliwość",
|
||||||
|
"maximumFrequency": "Maksymalna Częstotliwość",
|
||||||
|
"frequencyScale": "Skala Częstotliwości",
|
||||||
|
"sensitivity": "Czułość",
|
||||||
|
"weightingFilter": "Filtr Wagi",
|
||||||
|
"minimumDecibels": "Minimum Decybeli",
|
||||||
|
"maximumDecibels": "Maksimum Decybeli",
|
||||||
|
"linearAmplitude": "Amplituda Linearna",
|
||||||
|
"linearBoost": "Podbicie Linearne",
|
||||||
|
"peakBehavior": "Zachowanie Szczytów",
|
||||||
|
"showPeaks": "Pokaż Szczyty",
|
||||||
|
"fadePeaks": "Zanikaj Sczyty",
|
||||||
|
"peakLine": "Linia Szczytów",
|
||||||
|
"gravity": "Grawitacja",
|
||||||
|
"peakFadeTime": "Czas Zanikania Szczytów (ms)",
|
||||||
|
"peakHoldTime": "Czas Utrzymywania Szczytu (ms)",
|
||||||
|
"radialSpectrum": "Spektrum Promieniowe",
|
||||||
|
"radial": "Promieniowe",
|
||||||
|
"radialInvert": "Odwrócenie Promieniowe",
|
||||||
|
"spinSpeed": "Prędkość Obrotu",
|
||||||
|
"radius": "Promień",
|
||||||
|
"reflexMirror": "Lustro refleksyjne",
|
||||||
|
"reflexFit": "Dopasowanie Odbić",
|
||||||
|
"reflexRatio": "Współczynnik Odbić",
|
||||||
|
"reflexAlpha": "Alpha Odbić",
|
||||||
|
"reflexBrightness": "Jasność Odbić",
|
||||||
|
"mirror": "Odbij lustrzanie",
|
||||||
|
"miscellaneousSettings": "Różne Ustawienia",
|
||||||
|
"alphaBars": "Alpha Pasków",
|
||||||
|
"ledBars": "Paski LED",
|
||||||
|
"trueLeds": "Prawdziwe LEDy",
|
||||||
|
"lumiBars": "Paski Lumi",
|
||||||
|
"outlineBars": "Obwódki Pasków",
|
||||||
|
"roundBars": "Zaokrąglone Paski",
|
||||||
|
"lowResolution": "Niska Rozdzielczość",
|
||||||
|
"splitGradient": "Rozdziel Gradient",
|
||||||
|
"showFPS": "Pokaż FPS",
|
||||||
|
"showScaleX": "Pokaż Skalę X",
|
||||||
|
"noteLabels": "Etykiety Nut",
|
||||||
|
"showScaleY": "Pokaż Skalę Y",
|
||||||
|
"options": {
|
||||||
|
"mode": {
|
||||||
|
"bars": "[0] Pasków",
|
||||||
|
"circle": "[1] Kółko",
|
||||||
|
"wave": "[2] Fala",
|
||||||
|
"rainbow": "[3] Tęcza",
|
||||||
|
"rings": "[4] Pierścienie",
|
||||||
|
"mirror": "[5] Lustro",
|
||||||
|
"line": "[6] Linia",
|
||||||
|
"particles": "[7] Cząsteczki",
|
||||||
|
"fullOctave": "[8] Pełna oktawa / 10 pasm",
|
||||||
|
"outlineBars": "[10] Paski z obwódką"
|
||||||
|
},
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"barIndex": "Indeks-Paska",
|
||||||
|
"barLevel": "Poziom-Paska"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "Klasyczny",
|
||||||
|
"prism": "Pryzmat",
|
||||||
|
"rainbow": "Tęcza",
|
||||||
|
"steelblue": "Stalowoniebieski",
|
||||||
|
"orangered": "Pomarańczowo-czerwony"
|
||||||
|
},
|
||||||
|
"channelLayout": {
|
||||||
|
"single": "Pojedynczy",
|
||||||
|
"dualCombined": "Podwójne-Połączone",
|
||||||
|
"dualHorizontal": "Podwójne-Poziome",
|
||||||
|
"dualVertical": "Podwójne-Pionowe"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"linear": "Linearne"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "Żadne",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,8 +356,6 @@
|
|||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "resolução da capa do álbum no reprodutor",
|
|
||||||
"playerAlbumArtResolution_description": "a resolução da pré-visualização da capa do álbum no reprodutor grande. Resoluções maiores deixam a imagem mais nítida, mas podem diminuir a velocidade de carregamento. O padrão é 0, ou seja, automático",
|
|
||||||
"playerbarOpenDrawer": "alternar tela cheia na barra do reprodutor",
|
"playerbarOpenDrawer": "alternar tela cheia na barra do reprodutor",
|
||||||
"playerbarOpenDrawer_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia",
|
"playerbarOpenDrawer_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia",
|
||||||
"remotePassword": "Senha do servidor de controle remoto",
|
"remotePassword": "Senha do servidor de controle remoto",
|
||||||
@@ -383,6 +381,8 @@
|
|||||||
"savePlayQueue_description": "Salvar a fila de reprodução ao fechar a aplicação e restaurá-la ao abrir a aplicação",
|
"savePlayQueue_description": "Salvar a fila de reprodução ao fechar a aplicação e restaurá-la ao abrir a aplicação",
|
||||||
"scrobble": "Scrobblar",
|
"scrobble": "Scrobblar",
|
||||||
"scrobble_description": "Scrobblar reproduções para o seu servidor de mídia",
|
"scrobble_description": "Scrobblar reproduções para o seu servidor de mídia",
|
||||||
|
"showRatings": "exibir avaliações por estrelas",
|
||||||
|
"showRatings_description": "exibir ou ocultar as avaliações por estrelas",
|
||||||
"showSkipButton": "Exibir botões de pular",
|
"showSkipButton": "Exibir botões de pular",
|
||||||
"showSkipButton_description": "Exibir ou ocultar os botões de pular na barra do reprodutor",
|
"showSkipButton_description": "Exibir ou ocultar os botões de pular na barra do reprodutor",
|
||||||
"showSkipButtons": "Exibir botões de pular",
|
"showSkipButtons": "Exibir botões de pular",
|
||||||
|
|||||||
@@ -670,7 +670,6 @@
|
|||||||
"playButtonBehavior": "поведение кнопки воспроизведения",
|
"playButtonBehavior": "поведение кнопки воспроизведения",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
|
|
||||||
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
|
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
|
||||||
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
|
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
|
||||||
"remotePort": "порт сервера удалённого управления",
|
"remotePort": "порт сервера удалённого управления",
|
||||||
@@ -711,7 +710,6 @@
|
|||||||
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
|
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
|
||||||
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
|
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
|
||||||
"playbackStyle": "стиль воспроизведения",
|
"playbackStyle": "стиль воспроизведения",
|
||||||
"playerAlbumArtResolution": "разрешение обложки альбома",
|
|
||||||
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
|
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
|
||||||
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
|
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
|
||||||
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
|
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
|
||||||
|
|||||||
@@ -654,8 +654,6 @@
|
|||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "rozlíšenie obrázka albumu",
|
|
||||||
"playerAlbumArtResolution_description": "rozlíšenie zobrazenia náhľadu veľkých obrázkov albumov. pri väčšom rozlíšení budú krajšie, ale môže sa spomaliť ich načítavanie. predvolené je 0, čo znamená automatické",
|
|
||||||
"playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača",
|
"playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača",
|
||||||
"playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku",
|
"playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku",
|
||||||
"remotePassword": "heslo servera vzdialeného ovládania",
|
"remotePassword": "heslo servera vzdialeného ovládania",
|
||||||
|
|||||||
@@ -33,7 +33,11 @@
|
|||||||
"musicbrainz": "Öppna i MusicBrainz"
|
"musicbrainz": "Öppna i MusicBrainz"
|
||||||
},
|
},
|
||||||
"createRadioStation": "skapa $t(entity.radioStation_one)",
|
"createRadioStation": "skapa $t(entity.radioStation_one)",
|
||||||
"deleteRadioStation": "ta bort $t(entity.radioStation_one)"
|
"deleteRadioStation": "ta bort $t(entity.radioStation_one)",
|
||||||
|
"addOrRemoveFromSelection": "lägg till eller ta bort från markerade",
|
||||||
|
"selectRangeOfItems": "välj en mängd objekt",
|
||||||
|
"selectAll": "markera alla",
|
||||||
|
"openApplicationDirectory": "öppna applikationskatalog"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "bakåt",
|
"backward": "bakåt",
|
||||||
@@ -143,7 +147,8 @@
|
|||||||
"clean": "städad",
|
"clean": "städad",
|
||||||
"gridRows": "rutnätsrader",
|
"gridRows": "rutnätsrader",
|
||||||
"tableColumns": "tabellkolumner",
|
"tableColumns": "tabellkolumner",
|
||||||
"itemsMore": "{{count}} fler"
|
"itemsMore": "{{count}} fler",
|
||||||
|
"countSelected": "{{count}} markerade"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
||||||
@@ -166,7 +171,12 @@
|
|||||||
"invalidServer": "ogiltig server",
|
"invalidServer": "ogiltig server",
|
||||||
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
|
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
|
||||||
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||||
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre"
|
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||||
|
"multipleServerSaveQueueError": "spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||||
|
"networkError": "en nätverksfel uppstod",
|
||||||
|
"notificationDenied": "åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||||
|
"openError": "kunde inte öppna filen",
|
||||||
|
"settingsSyncError": "diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "mest spelade",
|
"mostPlayed": "mest spelade",
|
||||||
@@ -209,7 +219,9 @@
|
|||||||
"album": "$t(entity.album_one)",
|
"album": "$t(entity.album_one)",
|
||||||
"trackNumber": "spår",
|
"trackNumber": "spår",
|
||||||
"songCount": "sångräkning",
|
"songCount": "sångräkning",
|
||||||
"criticRating": "kritikerbetyg"
|
"criticRating": "kritikerbetyg",
|
||||||
|
"albumCount": "$t(entity.album_other) antal",
|
||||||
|
"explicitStatus": "$t(common.explicitStatus)"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
@@ -236,13 +248,17 @@
|
|||||||
"input_savePassword": "spara lösenord",
|
"input_savePassword": "spara lösenord",
|
||||||
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
|
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
|
||||||
"ignoreCors": "ignorera cors ($t(common.restartRequired))",
|
"ignoreCors": "ignorera cors ($t(common.restartRequired))",
|
||||||
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas"
|
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas",
|
||||||
|
"input_preferInstantMix": "föredra instant mixning",
|
||||||
|
"input_preferInstantMixDescription": "använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"success": "tillade {{message}} $t(entity.track_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
|
"success": "lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "lägg till i $t(entity.playlist_one)",
|
"title": "lägg till i $t(entity.playlist_one)",
|
||||||
"input_skipDuplicates": "hoppa över dubbletter",
|
"input_skipDuplicates": "hoppa över dubbletter",
|
||||||
"input_playlists": "$t(entity.playlist_other)"
|
"input_playlists": "$t(entity.playlist_other)",
|
||||||
|
"create": "skapa $t(entity.playlist_one) {{playlist}}",
|
||||||
|
"searchOrCreate": "sök $t(entity.playlist_other) eller skriv för att skapa en ny"
|
||||||
},
|
},
|
||||||
"updateServer": {
|
"updateServer": {
|
||||||
"title": "uppdatera server",
|
"title": "uppdatera server",
|
||||||
@@ -258,7 +274,19 @@
|
|||||||
"title": "sångtext sök"
|
"title": "sångtext sök"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "redigera $t(entity.playlist_one)"
|
"title": "redigera $t(entity.playlist_one)",
|
||||||
|
"publicJellyfinNote": "Jellyfin visar av någon anledning inte om en spellista är publik eller inte. Om du önskar att denna ska förbli publik, så får du ha följande indata markerade"
|
||||||
|
},
|
||||||
|
"largeFetchConfirmation": {
|
||||||
|
"title": "lägg till objekt till kön",
|
||||||
|
"description": "Åtgärden kommer att lägga till alla objekt till den nuvarande filtrerade vyn"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"success": "radiostation skapades",
|
||||||
|
"title": "skapa radiostation",
|
||||||
|
"input_homepageUrl": "hemside-URL",
|
||||||
|
"input_name": "namn",
|
||||||
|
"input_streamUrl": "stream url"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -306,7 +334,17 @@
|
|||||||
"addFavorite": "$t(action.addToFavorites)",
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
"play": "$t(player.play)",
|
"play": "$t(player.play)",
|
||||||
"numberSelected": "{{count}} vald",
|
"numberSelected": "{{count}} vald",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)"
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
|
"download": "ladda ner",
|
||||||
|
"moveItems": "$t(action.moveItems)",
|
||||||
|
"moveToNext": "$t(action.moveToNext)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
|
"playShuffled": "$t(player.shuffle)",
|
||||||
|
"shareItem": "dela objekt",
|
||||||
|
"goTo": "gå till",
|
||||||
|
"goToAlbum": "gå till $t(entity.album_one)",
|
||||||
|
"goToAlbumArtist": "gå till $t(entity.albumArtist_one)",
|
||||||
|
"showDetails": "hämta information"
|
||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "mer från $t(entity.artist_one)",
|
"moreFromArtist": "mer från $t(entity.artist_one)",
|
||||||
@@ -340,6 +378,12 @@
|
|||||||
"searchFor": "sök efter {{query}}"
|
"searchFor": "sök efter {{query}}"
|
||||||
},
|
},
|
||||||
"title": "kommandon"
|
"title": "kommandon"
|
||||||
|
},
|
||||||
|
"manageServers": {
|
||||||
|
"url": "URL",
|
||||||
|
"username": "användarnamn",
|
||||||
|
"editServerDetailsTooltip": "redigera serverinställningar",
|
||||||
|
"removeServer": "ta bort server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -405,5 +449,32 @@
|
|||||||
"queue_moveToBottom": "flytta markerad till toppen",
|
"queue_moveToBottom": "flytta markerad till toppen",
|
||||||
"addLast": "lägg till sist",
|
"addLast": "lägg till sist",
|
||||||
"mute": "muta"
|
"mute": "muta"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "min",
|
||||||
|
"secondShort": "sek",
|
||||||
|
"hourShort": "h",
|
||||||
|
"dayShort": "dag"
|
||||||
|
},
|
||||||
|
"filterOperator": {
|
||||||
|
"after": "är efter",
|
||||||
|
"afterDate": "är efter (datum)",
|
||||||
|
"before": "är före",
|
||||||
|
"beforeDate": "är före (datum)",
|
||||||
|
"contains": "innehåller",
|
||||||
|
"endsWith": "slutar med",
|
||||||
|
"inPlaylist": "är inom",
|
||||||
|
"inTheLast": "är i den sista",
|
||||||
|
"inTheRange": "är i spannet",
|
||||||
|
"inTheRangeDate": "är i spannet (datum)",
|
||||||
|
"is": "är",
|
||||||
|
"isNot": "är inte",
|
||||||
|
"isGreaterThan": "är större än",
|
||||||
|
"isLessThan": "är mindre än",
|
||||||
|
"matchesRegex": "matchar regex",
|
||||||
|
"notContains": "innehåller inte",
|
||||||
|
"notInPlaylist": "är inte inom",
|
||||||
|
"notInTheLast": "är inte inom den sista",
|
||||||
|
"startsWith": "startar med"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -550,8 +550,6 @@
|
|||||||
"playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது",
|
"playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது",
|
||||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playerAlbumArtResolution": "பிளேயர் ஆல்பம் கலைத் தீர்மானம்",
|
|
||||||
"playerAlbumArtResolution_description": "பெரிய வீரரின் ஆல்பம் கலை முன்னோட்டத்திற்கான தீர்மானம். பெரியது இது மிகவும் மிருதுவானதாக தோற்றமளிக்கிறது, ஆனால் மெதுவாக ஏற்றுவதை மெதுவாகக் கொண்டிருக்கலாம். இயல்புநிலை 0 க்கு, அதாவது ஆட்டோ",
|
|
||||||
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
|
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
|
||||||
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
|
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
|
||||||
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
|
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
|
||||||
|
|||||||
@@ -21,7 +21,15 @@
|
|||||||
"goToPage": "sayfaya git",
|
"goToPage": "sayfaya git",
|
||||||
"moveToNext": "sonrakine geç",
|
"moveToNext": "sonrakine geç",
|
||||||
"refresh": "$t(common.refresh)",
|
"refresh": "$t(common.refresh)",
|
||||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç"
|
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç",
|
||||||
|
"addOrRemoveFromSelection": "seçime ekle veya seçimi kaldır",
|
||||||
|
"selectRangeOfItems": "bir dizi öğe seçin",
|
||||||
|
"createRadioStation": "$t(entity.radioStation_one) oluştur",
|
||||||
|
"deleteRadioStation": "$t(entity.radioStation_one) istasyonunu sil",
|
||||||
|
"selectAll": "tümünü seç",
|
||||||
|
"downloadStarted": "{{count}} öğenin indirilmesine başlandı",
|
||||||
|
"moveUp": "yukarı kaydır",
|
||||||
|
"moveDown": "aşağı kaydır"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"action_one": "eylem",
|
"action_one": "eylem",
|
||||||
@@ -120,7 +128,8 @@
|
|||||||
"trackGain": "parça kazancı",
|
"trackGain": "parça kazancı",
|
||||||
"trackPeak": "parça zirvesi",
|
"trackPeak": "parça zirvesi",
|
||||||
"private": "gizli",
|
"private": "gizli",
|
||||||
"clean": "temiz"
|
"clean": "temiz",
|
||||||
|
"countSelected": "{{count}} adet seçildi"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "albüm",
|
"album_one": "albüm",
|
||||||
@@ -604,8 +613,6 @@
|
|||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "oynatıcı albüm resmi çözünürlüğü",
|
|
||||||
"playerAlbumArtResolution_description": "büyük oynatıcının albüm resmi önizlemesi için çözünürlük. daha büyük değerler daha net görünmesini sağlar, ancak yüklemeyi yavaşlatabilir. varsayılan değer 0, otomatik olarak çalışır",
|
|
||||||
"playerbarOpenDrawer": "oynatma çubuğu tam ekran geçişi",
|
"playerbarOpenDrawer": "oynatma çubuğu tam ekran geçişi",
|
||||||
"playerbarOpenDrawer_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir",
|
"playerbarOpenDrawer_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir",
|
||||||
"remotePassword": "uzaktan kontrol sunucusu şifresi",
|
"remotePassword": "uzaktan kontrol sunucusu şifresi",
|
||||||
|
|||||||
@@ -31,7 +31,12 @@
|
|||||||
"shuffle": "随机播放",
|
"shuffle": "随机播放",
|
||||||
"shuffleAll": "随机播放全部",
|
"shuffleAll": "随机播放全部",
|
||||||
"shuffleSelected": "随机播放选定的内容",
|
"shuffleSelected": "随机播放选定的内容",
|
||||||
"viewMore": "查看更多"
|
"viewMore": "查看更多",
|
||||||
|
"addOrRemoveFromSelection": "在所选内容中添加或移除",
|
||||||
|
"selectRangeOfItems": "批量选择",
|
||||||
|
"selectAll": "全选",
|
||||||
|
"createRadioStation": "创建$t(entity.radioStation_one)",
|
||||||
|
"deleteRadioStation": "删除$t(entity.radioStation_one)"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"increase": "增高",
|
"increase": "增高",
|
||||||
@@ -189,7 +194,7 @@
|
|||||||
"queue_moveToTop": "将所选项移至底部",
|
"queue_moveToTop": "将所选项移至底部",
|
||||||
"queue_moveToBottom": "将所选项移至顶部",
|
"queue_moveToBottom": "将所选项移至顶部",
|
||||||
"shuffle_off": "禁用随机播放",
|
"shuffle_off": "禁用随机播放",
|
||||||
"addLast": "添加至播放列表末尾",
|
"addLast": "上一曲",
|
||||||
"mute": "静音",
|
"mute": "静音",
|
||||||
"skip_forward": "向前跳过",
|
"skip_forward": "向前跳过",
|
||||||
"playbackSpeed": "播放速度",
|
"playbackSpeed": "播放速度",
|
||||||
@@ -367,8 +372,6 @@
|
|||||||
"startMinimized_description": "在系统托盘中启动应用程序",
|
"startMinimized_description": "在系统托盘中启动应用程序",
|
||||||
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置",
|
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置",
|
||||||
"clearCacheSuccess": "缓存清除成功",
|
"clearCacheSuccess": "缓存清除成功",
|
||||||
"playerAlbumArtResolution": "播放器专辑封面分辨率",
|
|
||||||
"playerAlbumArtResolution_description": "大型播放器专辑封面预览的分辨率。较大使其看起来更清晰,但可能会减慢加载速度。默认为0,表示自动",
|
|
||||||
"homeConfiguration": "主页配置",
|
"homeConfiguration": "主页配置",
|
||||||
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
||||||
"passwordStore": "密码/密钥存储",
|
"passwordStore": "密码/密钥存储",
|
||||||
|
|||||||
@@ -106,7 +106,9 @@
|
|||||||
"explicitStatus": "Explicit狀態",
|
"explicitStatus": "Explicit狀態",
|
||||||
"explicit": "Explicit",
|
"explicit": "Explicit",
|
||||||
"gridRows": "網格行",
|
"gridRows": "網格行",
|
||||||
"noFilters": "未設定任何過濾器"
|
"noFilters": "未設定任何過濾器",
|
||||||
|
"countSelected": "{{count}}個已選取",
|
||||||
|
"retry": "重試"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||||
@@ -134,7 +136,10 @@
|
|||||||
"notificationDenied": "通知權限被拒絕。此設定無效",
|
"notificationDenied": "通知權限被拒絕。此設定無效",
|
||||||
"openError": "無法開啟檔案",
|
"openError": "無法開啟檔案",
|
||||||
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
|
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
|
||||||
"saveQueueFailed": "儲存播放佇列失敗"
|
"saveQueueFailed": "儲存播放佇列失敗",
|
||||||
|
"settingsSyncError": "偵測到渲染器與主程序之間的設定不一致,請重新啟動應用程式以套用變更",
|
||||||
|
"noNetwork": "伺服器無法連線",
|
||||||
|
"noNetworkDescription": "無法連接到此伺服器"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
@@ -248,7 +253,8 @@
|
|||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"queryBuilder": "查詢建構器",
|
"queryBuilder": "查詢建構器",
|
||||||
"playerFilters": "播放過濾器",
|
"playerFilters": "播放過濾器",
|
||||||
"logger": "日誌記錄器"
|
"logger": "日誌記錄器",
|
||||||
|
"lyricsDisplay": "歌詞顯示"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
@@ -367,7 +373,9 @@
|
|||||||
"holdToShuffle": "按住以隨機",
|
"holdToShuffle": "按住以隨機",
|
||||||
"lyrics": "歌詞",
|
"lyrics": "歌詞",
|
||||||
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
||||||
"saveQueueToServer": "將播放佇列儲存至伺服器"
|
"saveQueueToServer": "將播放佇列儲存至伺服器",
|
||||||
|
"artistRadio": "藝人電台",
|
||||||
|
"trackRadio": "曲目電台"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
||||||
@@ -403,7 +411,7 @@
|
|||||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||||
"enableRemote": "啟用遠端控制伺服器",
|
"enableRemote": "啟用遠端控制伺服器",
|
||||||
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
||||||
"exitToTray": "退出時最小化到系統匣",
|
"exitToTray": "關閉時到將視窗最小化",
|
||||||
"followLyric": "跟隨目前歌詞",
|
"followLyric": "跟隨目前歌詞",
|
||||||
"font_description": "設定應用程式使用的字體",
|
"font_description": "設定應用程式使用的字體",
|
||||||
"fontType": "字體類型",
|
"fontType": "字體類型",
|
||||||
@@ -448,7 +456,7 @@
|
|||||||
"lyricOffset": "歌詞偏移(毫秒)",
|
"lyricOffset": "歌詞偏移(毫秒)",
|
||||||
"lyricOffset_description": "將歌詞偏移指定的毫秒數",
|
"lyricOffset_description": "將歌詞偏移指定的毫秒數",
|
||||||
"lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序",
|
"lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序",
|
||||||
"minimizeToTray": "最小化到匣",
|
"minimizeToTray": "最小化到系統匣",
|
||||||
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
||||||
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
||||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
||||||
@@ -572,11 +580,9 @@
|
|||||||
"passwordStore": "密碼/secret儲存",
|
"passwordStore": "密碼/secret儲存",
|
||||||
"passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目",
|
"passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerAlbumArtResolution": "播放器專輯封面解析度",
|
|
||||||
"playerAlbumArtResolution_description": "大型播放器專輯封面預覽的解析度。較大的解析度使其看起來更清晰,但可能會減慢載入速度。預設為 0,表示自動",
|
|
||||||
"playerbarOpenDrawer": "播放器列全螢幕切換",
|
"playerbarOpenDrawer": "播放器列全螢幕切換",
|
||||||
"playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器",
|
"playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器",
|
||||||
"startMinimized": "最小化啟動",
|
"startMinimized": "啟動時最小化",
|
||||||
"startMinimized_description": "在系統匣中啟動應用程式",
|
"startMinimized_description": "在系統匣中啟動應用程式",
|
||||||
"transcode_description": "啟用轉碼到不同格式",
|
"transcode_description": "啟用轉碼到不同格式",
|
||||||
"transcodeBitrate": "要轉碼的比特率",
|
"transcodeBitrate": "要轉碼的比特率",
|
||||||
@@ -677,7 +683,17 @@
|
|||||||
"logLevel_optionInfo": "Info",
|
"logLevel_optionInfo": "Info",
|
||||||
"logLevel_optionWarn": "Warn",
|
"logLevel_optionWarn": "Warn",
|
||||||
"useThemeAccentColor": "使用主題強調色",
|
"useThemeAccentColor": "使用主題強調色",
|
||||||
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色"
|
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色",
|
||||||
|
"artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量",
|
||||||
|
"imageResolution": "圖片解析度",
|
||||||
|
"imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度",
|
||||||
|
"imageResolution_optionTable": "表格",
|
||||||
|
"imageResolution_optionItemCard": "項目卡片",
|
||||||
|
"imageResolution_optionSidebar": "側邊欄",
|
||||||
|
"imageResolution_optionHeader": "頁首",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "全螢幕播放器",
|
||||||
|
"combinedLyricsAndVisualizer_description": "將歌詞與視覺化效果整合至同一個面板",
|
||||||
|
"combinedLyricsAndVisualizer": "在播放器側邊欄整合歌詞與視覺化效果"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -817,7 +833,10 @@
|
|||||||
"holdToMoveToTop": "按住以移動至頂部",
|
"holdToMoveToTop": "按住以移動至頂部",
|
||||||
"holdToMoveToBottom": "按住以移動至底部",
|
"holdToMoveToBottom": "按住以移動至底部",
|
||||||
"createRadioStation": "創建 $t(entity.radioStation_one)",
|
"createRadioStation": "創建 $t(entity.radioStation_one)",
|
||||||
"deleteRadioStation": "刪除 $t(entity.radioStation_one)"
|
"deleteRadioStation": "刪除 $t(entity.radioStation_one)",
|
||||||
|
"openApplicationDirectory": "開啟應用程式目錄",
|
||||||
|
"addOrRemoveFromSelection": "新增或移除選取項目",
|
||||||
|
"selectAll": "全選"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_other": "專輯",
|
"album_other": "專輯",
|
||||||
@@ -984,6 +1003,11 @@
|
|||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "已將播放佇列儲存至伺服器"
|
"success": "已將播放佇列儲存至伺服器"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"export": "匯出歌詞",
|
||||||
|
"input_synced": "匯出同步歌詞",
|
||||||
|
"input_offset": "$t(setting.lyricOffset)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
@@ -1035,5 +1059,14 @@
|
|||||||
"notContains": "不包含",
|
"notContains": "不包含",
|
||||||
"notInPlaylist": "不在…之中",
|
"notInPlaylist": "不在…之中",
|
||||||
"startsWith": "以…開頭"
|
"startsWith": "以…開頭"
|
||||||
|
},
|
||||||
|
"datetime": {
|
||||||
|
"minuteShort": "分",
|
||||||
|
"secondShort": "秒",
|
||||||
|
"hourShort": "小時",
|
||||||
|
"dayShort": "天"
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"visualizerType": "視覺化效果類型"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,6 +657,9 @@ if (mprisPlayer) {
|
|||||||
}
|
}
|
||||||
currentState.volume = volume;
|
currentState.volume = volume;
|
||||||
broadcast({ data: volume, event: 'volume' });
|
broadcast({ data: volume, event: 'volume' });
|
||||||
|
getMainWindow()?.webContents.send('request-volume', {
|
||||||
|
volume,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,39 @@ import type { TitleTheme } from '/@/shared/types/types';
|
|||||||
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
|
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
export const store = new Store({
|
const getFrame = () => {
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
const isMacOS = process.platform === 'darwin';
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
return 'windows';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMacOS) {
|
||||||
|
return 'macOS';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'linux';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = new Store<any>({
|
||||||
beforeEachMigration: (_store, context) => {
|
beforeEachMigration: (_store, context) => {
|
||||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||||
},
|
},
|
||||||
|
defaults: {
|
||||||
|
disable_auto_updates: false,
|
||||||
|
enableNeteaseTranslation: false,
|
||||||
|
global_media_hotkeys: true,
|
||||||
|
mediaSession: false,
|
||||||
|
playbackType: 'web',
|
||||||
|
should_prompt_accessibility: true,
|
||||||
|
shown_accessibility_warning: false,
|
||||||
|
window_enable_tray: true,
|
||||||
|
window_exit_to_tray: false,
|
||||||
|
window_minimize_to_tray: false,
|
||||||
|
window_start_minimized: false,
|
||||||
|
window_window_bar_style: getFrame(),
|
||||||
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
'>=0.21.2': (store) => {
|
'>=0.21.2': (store) => {
|
||||||
store.set('window_bar_style', 'linux');
|
store.set('window_bar_style', 'linux');
|
||||||
|
|||||||
@@ -141,48 +141,48 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
|||||||
mprisPlayer.shuffle = shuffle;
|
mprisPlayer.shuffle = shuffle;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
ipcMain.on(
|
||||||
try {
|
'update-song',
|
||||||
if (!song?.id) {
|
(_event, song: QueueSong | undefined, imageUrl: null | string | undefined) => {
|
||||||
mprisPlayer.metadata = {};
|
try {
|
||||||
return;
|
if (!song?.id) {
|
||||||
|
mprisPlayer.metadata = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mprisPlayer.metadata = {
|
||||||
|
'mpris:artUrl': imageUrl || null,
|
||||||
|
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
||||||
|
'mpris:trackid': song.id
|
||||||
|
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
||||||
|
: '',
|
||||||
|
'xesam:album': song.album || null,
|
||||||
|
'xesam:albumArtist': song.albumArtists?.length
|
||||||
|
? song.albumArtists.map((artist) => artist.name)
|
||||||
|
: null,
|
||||||
|
'xesam:artist': song.artists?.length
|
||||||
|
? song.artists.map((artist) => artist.name)
|
||||||
|
: null,
|
||||||
|
'xesam:audioBpm': song.bpm,
|
||||||
|
// Comment is a `list of strings` type
|
||||||
|
'xesam:comment': song.comment ? [song.comment] : null,
|
||||||
|
'xesam:contentCreated': song.releaseDate,
|
||||||
|
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||||
|
'xesam:genre': song.genres?.length
|
||||||
|
? song.genres.map((genre: any) => genre.name)
|
||||||
|
: null,
|
||||||
|
'xesam:lastUsed': song.lastPlayedAt,
|
||||||
|
'xesam:title': song.name || null,
|
||||||
|
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||||
|
'xesam:useCount':
|
||||||
|
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
||||||
|
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
|
||||||
|
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const upsizedImageUrl = song.imageUrl
|
);
|
||||||
? song.imageUrl
|
|
||||||
?.replace(/&size=\d+/, '&size=300')
|
|
||||||
.replace(/\?width=\d+/, '?width=300')
|
|
||||||
.replace(/&height=\d+/, '&height=300')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
mprisPlayer.metadata = {
|
|
||||||
'mpris:artUrl': upsizedImageUrl,
|
|
||||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
|
||||||
'mpris:trackid': song.id
|
|
||||||
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
|
||||||
: '',
|
|
||||||
'xesam:album': song.album || null,
|
|
||||||
'xesam:albumArtist': song.albumArtists?.length
|
|
||||||
? song.albumArtists.map((artist) => artist.name)
|
|
||||||
: null,
|
|
||||||
'xesam:artist': song.artists?.length ? song.artists.map((artist) => artist.name) : null,
|
|
||||||
'xesam:audioBpm': song.bpm,
|
|
||||||
// Comment is a `list of strings` type
|
|
||||||
'xesam:comment': song.comment ? [song.comment] : null,
|
|
||||||
'xesam:contentCreated': song.releaseDate,
|
|
||||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
|
||||||
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
|
|
||||||
'xesam:lastUsed': song.lastPlayedAt,
|
|
||||||
'xesam:title': song.name || null,
|
|
||||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
|
||||||
'xesam:useCount':
|
|
||||||
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
|
||||||
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
|
|
||||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export { mprisPlayer };
|
export { mprisPlayer };
|
||||||
|
|||||||
+2
-1
@@ -30,6 +30,7 @@ import MenuBuilder from './menu';
|
|||||||
import {
|
import {
|
||||||
autoUpdaterLogInterface,
|
autoUpdaterLogInterface,
|
||||||
createLog,
|
createLog,
|
||||||
|
disableAutoUpdates,
|
||||||
hotkeyToElectronAccelerator,
|
hotkeyToElectronAccelerator,
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
@@ -456,7 +457,7 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (store.get('disable_auto_updates') !== true) {
|
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const disableAutoUpdates = () => {
|
||||||
|
return process.env['DISABLE_AUTO_UPDATES'];
|
||||||
|
};
|
||||||
|
|
||||||
export const isMacOS = () => {
|
export const isMacOS = () => {
|
||||||
return process.platform === 'darwin';
|
return process.platform === 'darwin';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const updateShuffle = (shuffle: boolean) => {
|
|||||||
ipcRenderer.send('update-shuffle', shuffle);
|
ipcRenderer.send('update-shuffle', shuffle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSong = (song: QueueSong | undefined) => {
|
const updateSong = (song: QueueSong | undefined, imageUrl?: null | string) => {
|
||||||
ipcRenderer.send('update-song', song);
|
ipcRenderer.send('update-song', song, imageUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||||
@@ -51,11 +51,16 @@ const requestToggleShuffle = (
|
|||||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||||
|
ipcRenderer.on('request-volume', cb);
|
||||||
|
};
|
||||||
|
|
||||||
export const mpris = {
|
export const mpris = {
|
||||||
requestPosition,
|
requestPosition,
|
||||||
requestSeek,
|
requestSeek,
|
||||||
requestToggleRepeat,
|
requestToggleRepeat,
|
||||||
requestToggleShuffle,
|
requestToggleShuffle,
|
||||||
|
requestVolume,
|
||||||
updatePosition,
|
updatePosition,
|
||||||
updateRepeat,
|
updateRepeat,
|
||||||
updateSeek,
|
updateSeek,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
|
|
||||||
import { isLinux, isMacOS, isWindows } from '../main/utils';
|
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/utils';
|
||||||
|
|
||||||
const openItem = async (path: string) => {
|
const openItem = async (path: string) => {
|
||||||
return ipcRenderer.invoke('open-item', path);
|
return ipcRenderer.invoke('open-item', path);
|
||||||
@@ -40,6 +40,7 @@ const download = (url: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
|
disableAutoUpdates,
|
||||||
download,
|
download,
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
|
|||||||
@@ -320,6 +320,20 @@ export const controller: GeneralController = {
|
|||||||
query: mergeMusicFolderId(args.query, server),
|
query: mergeMusicFolderId(args.query, server),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getArtistRadio(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'getArtistRadio',
|
||||||
|
server.type,
|
||||||
|
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||||
|
},
|
||||||
getDownloadUrl(args) {
|
getDownloadUrl(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
@@ -370,6 +384,20 @@ export const controller: GeneralController = {
|
|||||||
query: mergeMusicFolderId(args.query, server),
|
query: mergeMusicFolderId(args.query, server),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getImageUrl(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
apiController(
|
||||||
|
'getImageUrl',
|
||||||
|
server.type,
|
||||||
|
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }) || null
|
||||||
|
);
|
||||||
|
},
|
||||||
getInternetRadioStations(args) {
|
getInternetRadioStations(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
@@ -493,7 +521,11 @@ export const controller: GeneralController = {
|
|||||||
return apiController(
|
return apiController(
|
||||||
'getRandomSongList',
|
'getRandomSongList',
|
||||||
server.type,
|
server.type,
|
||||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
)?.({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { ...args.apiClientProps, server },
|
||||||
|
query: mergeMusicFolderId(args.query, server),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getRoles(args) {
|
getRoles(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
@@ -535,7 +567,11 @@ export const controller: GeneralController = {
|
|||||||
return apiController(
|
return apiController(
|
||||||
'getSimilarSongs',
|
'getSimilarSongs',
|
||||||
server.type,
|
server.type,
|
||||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
)?.({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { ...args.apiClientProps, server },
|
||||||
|
query: mergeMusicFolderId(args.query, server),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getSongDetail(args) {
|
getSongDetail(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
...artistQuery,
|
...artistQuery,
|
||||||
Fields: 'People, Tags',
|
Fields: 'People, Tags, Studios',
|
||||||
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
||||||
IncludeItemTypes: 'MusicAlbum',
|
IncludeItemTypes: 'MusicAlbum',
|
||||||
IsFavorite: query.favorite,
|
IsFavorite: query.favorite,
|
||||||
@@ -426,10 +426,31 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
apiClientProps,
|
apiClientProps,
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
|
getArtistRadio: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
// For Jellyfin, use instant mix for artist radio
|
||||||
|
const res = await jfApiClient(apiClientProps).getInstantMix({
|
||||||
|
params: {
|
||||||
|
itemId: query.artistId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||||
|
Limit: query.count,
|
||||||
|
UserId: apiClientProps.server?.userId || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist radio songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server));
|
||||||
|
},
|
||||||
getDownloadUrl: (args) => {
|
getDownloadUrl: (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
return `${apiClientProps.server?.url}/items/${query.id}/download?api_key=${apiClientProps.server?.credential}`;
|
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
||||||
},
|
},
|
||||||
getFolder: async ({ apiClientProps, query }) => {
|
getFolder: async ({ apiClientProps, query }) => {
|
||||||
const userId = apiClientProps.server?.userId;
|
const userId = apiClientProps.server?.userId;
|
||||||
@@ -670,6 +691,22 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: res.body?.TotalRecordCount || 0,
|
totalRecordCount: res.body?.TotalRecordCount || 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||||
|
const { id, size } = query;
|
||||||
|
const imageSize = size;
|
||||||
|
|
||||||
|
if (!server?.url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Jellyfin, we construct the URL pattern
|
||||||
|
// The server will return a 404 or placeholder if no image exists
|
||||||
|
const baseUrl = `${server.url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
|
||||||
|
|
||||||
|
// For songs, we might want to fall back to album art, but we don't have albumId here
|
||||||
|
// The caller can handle this if needed
|
||||||
|
return baseUrl;
|
||||||
|
},
|
||||||
getInternetRadioStations: async (args) => {
|
getInternetRadioStations: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
@@ -886,7 +923,12 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get server info');
|
throw new Error('Failed to get server info');
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = getFeatures(VERSION_INFO, res.body.Version);
|
const defaultFeatures = {};
|
||||||
|
|
||||||
|
const features = {
|
||||||
|
...defaultFeatures,
|
||||||
|
...getFeatures(VERSION_INFO, res.body.Version),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
features,
|
features,
|
||||||
@@ -1077,9 +1119,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((item) =>
|
items: items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||||
jfNormalize.song(item, apiClientProps.server, query.imageSize),
|
|
||||||
),
|
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount,
|
totalRecordCount,
|
||||||
};
|
};
|
||||||
@@ -1093,7 +1133,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
const { bitrate, format, id, transcode } = query;
|
const { bitrate, format, id, transcode } = query;
|
||||||
const deviceId = '';
|
const deviceId = '';
|
||||||
|
|
||||||
let url = `${server?.url}/Items/${id}/Download?api_key=${server?.credential}&playSessionId=${deviceId}`;
|
let url = `${server?.url}/Items/${id}/Download?apiKey=${server?.credential}&playSessionId=${deviceId}`;
|
||||||
|
|
||||||
if (transcode) {
|
if (transcode) {
|
||||||
// Some format appears to be required. Fall back to trusty MP3 if not specified
|
// Some format appears to be required. Fall back to trusty MP3 if not specified
|
||||||
|
|||||||
@@ -322,13 +322,17 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
? query.genreIds
|
? query.genreIds
|
||||||
: query.genreIds?.[0];
|
: query.genreIds?.[0];
|
||||||
|
|
||||||
|
const artistIds = hasFeature(apiClientProps.server, ServerFeature.BFR)
|
||||||
|
? query.artistIds
|
||||||
|
: query.artistIds?.[0];
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
artist_id: query.artistIds?.[0],
|
artist_id: artistIds,
|
||||||
compilation: query.compilation,
|
compilation: query.compilation,
|
||||||
genre_id: genres,
|
genre_id: genres,
|
||||||
has_rating: query.hasRating,
|
has_rating: query.hasRating,
|
||||||
@@ -401,6 +405,32 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
apiClientProps,
|
apiClientProps,
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
|
getArtistRadio: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
// Use getSimilarSongs2 API for artist radio
|
||||||
|
const res = await ssApiClient({
|
||||||
|
...apiClientProps,
|
||||||
|
silent: true,
|
||||||
|
}).getSimilarSongs2({
|
||||||
|
query: {
|
||||||
|
count: query.count,
|
||||||
|
id: query.artistId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist radio songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.body.similarSongs2?.song) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.similarSongs2.song.map((song) =>
|
||||||
|
ssNormalize.song(song, apiClientProps.server),
|
||||||
|
);
|
||||||
|
},
|
||||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||||
getFolder: SubsonicController.getFolder,
|
getFolder: SubsonicController.getFolder,
|
||||||
getGenreList: async (args) => {
|
getGenreList: async (args) => {
|
||||||
@@ -461,6 +491,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getImageUrl: SubsonicController.getImageUrl,
|
||||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
||||||
getLyrics: SubsonicController.getLyrics,
|
getLyrics: SubsonicController.getLyrics,
|
||||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||||
@@ -664,9 +695,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((song) =>
|
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)),
|
||||||
ndNormalize.song(song, apiClientProps.server, query.imageSize),
|
|
||||||
),
|
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
AlbumDetailQuery,
|
AlbumDetailQuery,
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
ArtistListQuery,
|
ArtistListQuery,
|
||||||
|
ArtistRadioQuery,
|
||||||
FolderQuery,
|
FolderQuery,
|
||||||
GenreListQuery,
|
GenreListQuery,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
@@ -340,6 +341,10 @@ export const queryKeys: Record<
|
|||||||
root: (serverId: string) => [serverId] as const,
|
root: (serverId: string) => [serverId] as const,
|
||||||
},
|
},
|
||||||
songs: {
|
songs: {
|
||||||
|
artistRadio: (serverId: string, query?: ArtistRadioQuery) => {
|
||||||
|
if (query) return [serverId, 'songs', 'artistRadio', query] as const;
|
||||||
|
return [serverId, 'songs', 'artistRadio'] as const;
|
||||||
|
},
|
||||||
count: (serverId: string, query?: SongListQuery) => {
|
count: (serverId: string, query?: SongListQuery) => {
|
||||||
const { filter, pagination } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ export const contract = c.router({
|
|||||||
200: ssType._response.similarSongs,
|
200: ssType._response.similarSongs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getSimilarSongs2: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getSimilarSongs2',
|
||||||
|
query: ssType._parameters.similarSongs2,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.similarSongs2,
|
||||||
|
},
|
||||||
|
},
|
||||||
getSong: {
|
getSong: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getSong.view',
|
path: 'getSong.view',
|
||||||
|
|||||||
@@ -155,7 +155,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
const res = await ssApiClient(apiClientProps).createFavorite({
|
const res = await ssApiClient(apiClientProps).createFavorite({
|
||||||
query: {
|
query: {
|
||||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
artistId:
|
||||||
|
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||||
|
? query.id
|
||||||
|
: undefined,
|
||||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -205,7 +208,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
const res = await ssApiClient(apiClientProps).removeFavorite({
|
const res = await ssApiClient(apiClientProps).removeFavorite({
|
||||||
query: {
|
query: {
|
||||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
artistId:
|
||||||
|
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||||
|
? query.id
|
||||||
|
: undefined,
|
||||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -273,11 +279,11 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
...ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||||
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||||
similarArtists:
|
similarArtists:
|
||||||
artistInfo?.similarArtist?.map((artist) =>
|
artistInfo?.similarArtist?.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||||
) || null,
|
) || null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -297,7 +303,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
||||||
|
|
||||||
let results = artists.map((artist) =>
|
let results = artists.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (query.searchTerm) {
|
if (query.searchTerm) {
|
||||||
@@ -348,6 +354,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: query.startIndex,
|
albumOffset: query.startIndex,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: 0,
|
songCount: 0,
|
||||||
songOffset: 0,
|
songOffset: 0,
|
||||||
@@ -482,7 +489,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
return {
|
return {
|
||||||
items:
|
items:
|
||||||
res.body.albumList2.album?.map((album) =>
|
res.body.albumList2.album?.map((album) =>
|
||||||
ssNormalize.album(album, apiClientProps.server, 300),
|
ssNormalize.album(album, apiClientProps.server),
|
||||||
) || [],
|
) || [],
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: null,
|
totalRecordCount: null,
|
||||||
@@ -503,6 +510,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: startIndex,
|
albumOffset: startIndex,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: 0,
|
songCount: 0,
|
||||||
songOffset: 0,
|
songOffset: 0,
|
||||||
@@ -652,7 +660,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let results = artists.map((artist) =>
|
let results = artists.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (query.searchTerm) {
|
if (query.searchTerm) {
|
||||||
@@ -676,6 +684,28 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
...args,
|
...args,
|
||||||
query: { ...args.query, startIndex: 0 },
|
query: { ...args.query, startIndex: 0 },
|
||||||
}).then((res) => res!.totalRecordCount!),
|
}).then((res) => res!.totalRecordCount!),
|
||||||
|
getArtistRadio: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).getSimilarSongs2({
|
||||||
|
query: {
|
||||||
|
count: query.count,
|
||||||
|
id: query.artistId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist radio songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.body.similarSongs2?.song) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.similarSongs2.song.map((song) =>
|
||||||
|
ssNormalize.song(song, apiClientProps.server),
|
||||||
|
);
|
||||||
|
},
|
||||||
getDownloadUrl: (args) => {
|
getDownloadUrl: (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -821,6 +851,28 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||||
|
const { id, size } = query;
|
||||||
|
const imageSize = size;
|
||||||
|
|
||||||
|
if (!server?.url || !server?.credential) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for default placeholder image ID
|
||||||
|
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
`${server.url}/rest/getCoverArt.view` +
|
||||||
|
`?id=${id}` +
|
||||||
|
`&${server.credential}` +
|
||||||
|
'&v=1.13.0' +
|
||||||
|
'&c=Feishin' +
|
||||||
|
(imageSize ? `&size=${imageSize}` : '')
|
||||||
|
);
|
||||||
|
},
|
||||||
getInternetRadioStations: async (args) => {
|
getInternetRadioStations: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
@@ -852,6 +904,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistDetail: async (args) => {
|
getPlaylistDetail: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -867,7 +920,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistList: async ({ apiClientProps, query }) => {
|
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
@@ -1145,6 +1197,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: 0,
|
albumOffset: 0,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: query.limit,
|
songCount: query.limit,
|
||||||
songOffset: query.startIndex,
|
songOffset: query.startIndex,
|
||||||
@@ -1289,6 +1342,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: 0,
|
albumOffset: 0,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: query.limit,
|
songCount: query.limit,
|
||||||
songOffset: query.startIndex,
|
songOffset: query.startIndex,
|
||||||
@@ -1329,6 +1383,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: 0,
|
albumOffset: 0,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: MAX_SUBSONIC_ITEMS,
|
songCount: MAX_SUBSONIC_ITEMS,
|
||||||
songOffset: startIndex,
|
songOffset: startIndex,
|
||||||
@@ -1432,6 +1487,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: 0,
|
albumOffset: 0,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: 1,
|
songCount: 1,
|
||||||
songOffset: sectionIndex,
|
songOffset: sectionIndex,
|
||||||
@@ -1460,6 +1516,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: 0,
|
albumOffset: 0,
|
||||||
artistCount: 0,
|
artistCount: 0,
|
||||||
artistOffset: 0,
|
artistOffset: 0,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.searchTerm || '',
|
query: query.searchTerm || '',
|
||||||
songCount: MAX_SUBSONIC_ITEMS,
|
songCount: MAX_SUBSONIC_ITEMS,
|
||||||
songOffset: startIndex,
|
songOffset: startIndex,
|
||||||
@@ -1729,6 +1786,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
albumOffset: query.albumStartIndex,
|
albumOffset: query.albumStartIndex,
|
||||||
artistCount: query.albumArtistLimit,
|
artistCount: query.albumArtistLimit,
|
||||||
artistOffset: query.albumArtistStartIndex,
|
artistOffset: query.albumArtistStartIndex,
|
||||||
|
musicFolderId: getLibraryId(query.musicFolderId),
|
||||||
query: query.query,
|
query: query.query,
|
||||||
songCount: query.songLimit,
|
songCount: query.songLimit,
|
||||||
songOffset: query.songStartIndex,
|
songOffset: query.songStartIndex,
|
||||||
|
|||||||
+8
-10
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import styles from './drag-preview.module.css';
|
import styles from './drag-preview.module.css';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { DragData } from '/@/shared/types/drag-and-drop';
|
import { DragData } from '/@/shared/types/drag-and-drop';
|
||||||
@@ -23,22 +24,19 @@ const getItemName = (item: unknown): string => {
|
|||||||
return 'Item';
|
return 'Item';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getItemImage = (item: unknown): null | string => {
|
|
||||||
if (item && typeof item === 'object') {
|
|
||||||
if ('imageUrl' in item && typeof item.imageUrl === 'string') {
|
|
||||||
return item.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DragPreview = memo(({ data }: DragPreviewProps) => {
|
export const DragPreview = memo(({ data }: DragPreviewProps) => {
|
||||||
const items = data.item || [];
|
const items = data.item || [];
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const itemCount = items.length;
|
const itemCount = items.length;
|
||||||
const firstItem = items[0];
|
const firstItem = items[0];
|
||||||
const itemName = firstItem ? getItemName(firstItem) : 'Item';
|
const itemName = firstItem ? getItemName(firstItem) : 'Item';
|
||||||
const itemImage = firstItem ? getItemImage(firstItem) : null;
|
|
||||||
|
const itemImage = useItemImageUrl({
|
||||||
|
id: (firstItem as { id: string })?.id,
|
||||||
|
itemType: data.itemType || LibraryItem.SONG,
|
||||||
|
type: 'table',
|
||||||
|
});
|
||||||
|
|
||||||
const isMultiple = itemCount > 1;
|
const isMultiple = itemCount > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.artist {
|
.artist {
|
||||||
|
width: 100%;
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 0 0 8px rgb(0 0 0 / 50%);
|
text-shadow: 0 0 8px rgb(0 0 0 / 50%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { generatePath, Link } from 'react-router';
|
import { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
import styles from './feature-carousel.module.css';
|
import styles from './feature-carousel.module.css';
|
||||||
|
|
||||||
|
import { ItemImage, useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||||
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
||||||
@@ -15,7 +16,6 @@ import { useCurrentServer } from '/@/renderer/store';
|
|||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Badge } from '/@/shared/components/badge/badge';
|
import { Badge } from '/@/shared/components/badge/badge';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
@@ -78,9 +78,15 @@ interface CarouselItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CarouselItem = ({ album }: CarouselItemProps) => {
|
const CarouselItem = ({ album }: CarouselItemProps) => {
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: album.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
type: 'itemCard',
|
||||||
|
});
|
||||||
|
|
||||||
const { background: backgroundColor } = useFastAverageColor({
|
const { background: backgroundColor } = useFastAverageColor({
|
||||||
algorithm: 'dominant',
|
algorithm: 'dominant',
|
||||||
src: album.imageUrl || null,
|
src: imageUrl || null,
|
||||||
srcLoaded: true,
|
srcLoaded: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,10 +116,12 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.imageSection}>
|
<div className={styles.imageSection}>
|
||||||
<Image
|
<ItemImage
|
||||||
className={styles.albumImage}
|
className={styles.albumImage}
|
||||||
containerClassName={styles.albumImageContainer}
|
containerClassName={styles.albumImageContainer}
|
||||||
src={album.imageUrl || undefined}
|
id={album.id}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
src={imageUrl}
|
||||||
/>
|
/>
|
||||||
<div className={styles.playButtonOverlay}>
|
<div className={styles.playButtonOverlay}>
|
||||||
<PlayButtonGroup onPlay={handlePlay} />
|
<PlayButtonGroup onPlay={handlePlay} />
|
||||||
@@ -123,7 +131,13 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
|||||||
<div className={styles.metadataSection}>
|
<div className={styles.metadataSection}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{album.albumArtists?.[0] && (
|
{album.albumArtists?.[0] && (
|
||||||
<Text className={styles.artist} fw={500} size="md">
|
<Text
|
||||||
|
className={styles.artist}
|
||||||
|
fw={500}
|
||||||
|
lineClamp={1}
|
||||||
|
size="md"
|
||||||
|
ta="center"
|
||||||
|
>
|
||||||
{album.albumArtists[0].name}
|
{album.albumArtists[0].name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -201,28 +215,70 @@ export const FeatureCarousel = ({ data, onNearEnd }: FeatureCarouselProps) => {
|
|||||||
}
|
}
|
||||||
}, [data, startIndex, itemsPerRow, onNearEnd]);
|
}, [data, startIndex, itemsPerRow, onNearEnd]);
|
||||||
|
|
||||||
const handleNext = (e?: MouseEvent<HTMLButtonElement>) => {
|
const handleNext = useCallback(
|
||||||
e?.preventDefault();
|
(e?: MouseEvent<HTMLButtonElement>) => {
|
||||||
e?.stopPropagation();
|
e?.preventDefault();
|
||||||
if (!data) return;
|
e?.stopPropagation();
|
||||||
directionRef.current = { isNext: true };
|
if (!data) return;
|
||||||
setStartIndex((prev) => (prev + itemsPerRow) % data.length);
|
directionRef.current = { isNext: true };
|
||||||
};
|
setStartIndex((prev) => (prev + itemsPerRow) % data.length);
|
||||||
|
},
|
||||||
|
[data, itemsPerRow],
|
||||||
|
);
|
||||||
|
|
||||||
const handlePrevious = (e?: MouseEvent<HTMLButtonElement>) => {
|
const handlePrevious = useCallback(
|
||||||
e?.preventDefault();
|
(e?: MouseEvent<HTMLButtonElement>) => {
|
||||||
e?.stopPropagation();
|
e?.preventDefault();
|
||||||
if (!data) return;
|
e?.stopPropagation();
|
||||||
directionRef.current = { isNext: false };
|
if (!data) return;
|
||||||
setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length);
|
directionRef.current = { isNext: false };
|
||||||
};
|
setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length);
|
||||||
|
},
|
||||||
|
[data, itemsPerRow],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canNavigate = data && data.length > itemsPerRow;
|
||||||
|
|
||||||
|
const wheelCooldownRef = useRef(0);
|
||||||
|
const wheelThreshold = 10;
|
||||||
|
const wheelCooldownMs = 250;
|
||||||
|
|
||||||
|
const handleWheel = useCallback(
|
||||||
|
(event: React.WheelEvent<HTMLDivElement>) => {
|
||||||
|
if (!canNavigate || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - wheelCooldownRef.current;
|
||||||
|
|
||||||
|
const horizontalDelta = Math.abs(event.deltaY);
|
||||||
|
|
||||||
|
if (horizontalDelta < wheelThreshold || elapsed < wheelCooldownMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.deltaY > 0) {
|
||||||
|
wheelCooldownRef.current = now;
|
||||||
|
handleNext();
|
||||||
|
} else if (event.deltaY < 0) {
|
||||||
|
wheelCooldownRef.current = now;
|
||||||
|
handlePrevious();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canNavigate, data, handleNext, handlePrevious, wheelCooldownMs, wheelThreshold],
|
||||||
|
);
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.carouselContainer} ref={containerRef}>
|
<div className={styles.carouselContainer} onWheel={handleWheel} ref={containerRef}>
|
||||||
<AnimatePresence initial={false} mode="popLayout">
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
<motion.div
|
<motion.div
|
||||||
animate="animate"
|
animate="animate"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface Card {
|
|||||||
|
|
||||||
interface GridCarouselProps {
|
interface GridCarouselProps {
|
||||||
cards: Card[];
|
cards: Card[];
|
||||||
|
enableRefresh?: boolean;
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
loadNextPage?: () => void;
|
loadNextPage?: () => void;
|
||||||
onNextPage: (page: number) => void;
|
onNextPage: (page: number) => void;
|
||||||
@@ -46,6 +47,7 @@ const pageVariants: Variants = {
|
|||||||
function BaseGridCarousel(props: GridCarouselProps) {
|
function BaseGridCarousel(props: GridCarouselProps) {
|
||||||
const {
|
const {
|
||||||
cards,
|
cards,
|
||||||
|
enableRefresh = false,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
@@ -155,45 +157,65 @@ function BaseGridCarousel(props: GridCarouselProps) {
|
|||||||
{cq.isCalculated && (
|
{cq.isCalculated && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.navigation}>
|
<div className={styles.navigation}>
|
||||||
<Group gap="xs" justify="space-between" w="100%">
|
{typeof title === 'string' ? (
|
||||||
<Group gap="xs">
|
<Group gap="xs" justify="space-between" w="100%">
|
||||||
{typeof title === 'string' ? (
|
<Group gap="xs">
|
||||||
<TextTitle fw={700} isNoSelect order={3}>
|
<TextTitle fw={700} isNoSelect order={3}>
|
||||||
{title}
|
{title}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
) : (
|
{enableRefresh && onRefresh && (
|
||||||
title
|
<ActionIcon
|
||||||
)}
|
icon="refresh"
|
||||||
{onRefresh && (
|
iconProps={{ size: 'xs' }}
|
||||||
|
onClick={onRefresh}
|
||||||
|
size="xs"
|
||||||
|
tooltip={{ label: 'Refresh' }}
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" justify="end">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="refresh"
|
disabled={isPrevDisabled}
|
||||||
iconProps={{ size: 'md' }}
|
icon="arrowLeftS"
|
||||||
onClick={onRefresh}
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handlePrevPage}
|
||||||
size="xs"
|
size="xs"
|
||||||
tooltip={{ label: 'Refresh' }}
|
variant="subtle"
|
||||||
variant="transparent"
|
|
||||||
/>
|
/>
|
||||||
)}
|
<ActionIcon
|
||||||
|
disabled={isNextDisabled}
|
||||||
|
icon="arrowRightS"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs" justify="end">
|
) : (
|
||||||
<ActionIcon
|
<div className={styles.customTitleContainer}>
|
||||||
disabled={isPrevDisabled}
|
<div className={styles.customTitleContent}>{title}</div>
|
||||||
icon="arrowLeftS"
|
<Group gap="xs" justify="end">
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handlePrevPage}
|
disabled={isPrevDisabled}
|
||||||
size="xs"
|
icon="arrowLeftS"
|
||||||
variant="subtle"
|
iconProps={{ size: 'lg' }}
|
||||||
/>
|
onClick={handlePrevPage}
|
||||||
<ActionIcon
|
size="xs"
|
||||||
disabled={isNextDisabled}
|
variant="subtle"
|
||||||
icon="arrowRightS"
|
/>
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handleNextPage}
|
disabled={isNextDisabled}
|
||||||
size="xs"
|
icon="arrowRightS"
|
||||||
variant="subtle"
|
iconProps={{ size: 'lg' }}
|
||||||
/>
|
onClick={handleNextPage}
|
||||||
</Group>
|
size="xs"
|
||||||
</Group>
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -14,6 +14,19 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-title-container {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--theme-spacing-sm);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-title-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface ItemCardControlsProps {
|
|||||||
internalState?: ItemListStateActions;
|
internalState?: ItemListStateActions;
|
||||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
|
showRating: boolean;
|
||||||
type?: 'compact' | 'default' | 'poster';
|
type?: 'compact' | 'default' | 'poster';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +181,7 @@ export const ItemCardControls = ({
|
|||||||
internalState,
|
internalState,
|
||||||
item,
|
item,
|
||||||
itemType,
|
itemType,
|
||||||
|
showRating,
|
||||||
type = 'default',
|
type = 'default',
|
||||||
}: ItemCardControlsProps) => {
|
}: ItemCardControlsProps) => {
|
||||||
const playNowHandler = useMemo(
|
const playNowHandler = useMemo(
|
||||||
@@ -267,6 +269,7 @@ export const ItemCardControls = ({
|
|||||||
<FavoriteButton isFavorite={isFavorite} onClick={favoriteHandler} />
|
<FavoriteButton isFavorite={isFavorite} onClick={favoriteHandler} />
|
||||||
)}
|
)}
|
||||||
{controls?.onRating &&
|
{controls?.onRating &&
|
||||||
|
showRating &&
|
||||||
(item?._serverType === ServerType.NAVIDROME ||
|
(item?._serverType === ServerType.NAVIDROME ||
|
||||||
item?._serverType === ServerType.SUBSONIC) && (
|
item?._serverType === ServerType.SUBSONIC) && (
|
||||||
<RatingButton
|
<RatingButton
|
||||||
|
|||||||
@@ -179,8 +179,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--theme-spacing-xs);
|
padding: var(--theme-spacing-xs);
|
||||||
text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
|
background-color: alpha(var(--theme-colors-background), 0.5);
|
||||||
background-color: rgb(0 0 0 / 50%);
|
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition:
|
transition:
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import formatDuration from 'format-duration';
|
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
import { Fragment, memo, ReactNode, useState } from 'react';
|
import { Fragment, memo, ReactNode, useState } from 'react';
|
||||||
import { generatePath, Link } from 'react-router';
|
import { generatePath, Link } from 'react-router';
|
||||||
|
|
||||||
import styles from './item-card.module.css';
|
import styles from './item-card.module.css';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
||||||
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
||||||
import {
|
import {
|
||||||
@@ -17,8 +18,16 @@ import {
|
|||||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
import { useGeneralSettings } from '/@/renderer/store';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
import {
|
||||||
|
formatDateAbsolute,
|
||||||
|
formatDateAbsoluteUTC,
|
||||||
|
formatDateRelative,
|
||||||
|
formatDurationString,
|
||||||
|
formatRating,
|
||||||
|
} from '/@/renderer/utils/format';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Separator } from '/@/shared/components/separator/separator';
|
import { Separator } from '/@/shared/components/separator/separator';
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
@@ -67,6 +76,7 @@ export const ItemCard = ({
|
|||||||
type = 'poster',
|
type = 'poster',
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardProps) => {
|
}: ItemCardProps) => {
|
||||||
|
const { showRatings } = useGeneralSettings();
|
||||||
const imageUrl = getImageUrl(data);
|
const imageUrl = getImageUrl(data);
|
||||||
const rows = providedRows || [];
|
const rows = providedRows || [];
|
||||||
|
|
||||||
@@ -84,6 +94,7 @@ export const ItemCard = ({
|
|||||||
isRound={isRound}
|
isRound={isRound}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
showRating={showRatings}
|
||||||
withControls={withControls}
|
withControls={withControls}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -100,6 +111,7 @@ export const ItemCard = ({
|
|||||||
isRound={isRound}
|
isRound={isRound}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
showRating={showRatings}
|
||||||
withControls={withControls}
|
withControls={withControls}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -117,6 +129,7 @@ export const ItemCard = ({
|
|||||||
isRound={isRound}
|
isRound={isRound}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
showRating={showRatings}
|
||||||
withControls={withControls}
|
withControls={withControls}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -130,18 +143,20 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
|||||||
imageUrl: string | undefined;
|
imageUrl: string | undefined;
|
||||||
internalState?: ItemListStateActions;
|
internalState?: ItemListStateActions;
|
||||||
rows: DataRow[];
|
rows: DataRow[];
|
||||||
|
showRating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CompactItemCard = ({
|
const CompactItemCard = ({
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
|
enableDrag,
|
||||||
enableExpansion,
|
enableExpansion,
|
||||||
enableNavigation,
|
enableNavigation,
|
||||||
imageUrl,
|
|
||||||
internalState,
|
internalState,
|
||||||
isRound,
|
isRound,
|
||||||
itemType,
|
itemType,
|
||||||
rows,
|
rows,
|
||||||
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
@@ -151,6 +166,53 @@ const CompactItemCard = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
const isSelected = useItemSelectionState(internalState, itemRowId || undefined);
|
||||||
|
|
||||||
|
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
|
||||||
|
drag: {
|
||||||
|
getId: () => {
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
|
return draggedItems.map((item) => item.id);
|
||||||
|
},
|
||||||
|
getItem: () => {
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
|
return draggedItems;
|
||||||
|
},
|
||||||
|
itemType,
|
||||||
|
onDragStart: () => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
|
if (internalState) {
|
||||||
|
internalState.setDragging(draggedItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrop: () => {
|
||||||
|
if (internalState) {
|
||||||
|
internalState.setDragging([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
operation:
|
||||||
|
itemType === LibraryItem.QUEUE_SONG
|
||||||
|
? [DragOperation.REORDER, DragOperation.ADD]
|
||||||
|
: [DragOperation.ADD],
|
||||||
|
target: DragTarget.ALBUM,
|
||||||
|
},
|
||||||
|
isEnabled: !!enableDrag && !!data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemId = data && internalState ? data.id : undefined;
|
||||||
|
const isDraggingState = useItemDraggingState(internalState, itemId);
|
||||||
|
const isDragging = isDraggingState || isDraggingLocal;
|
||||||
|
|
||||||
const handleClick = useDoubleClick({
|
const handleClick = useDoubleClick({
|
||||||
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (!data || !controls || !internalState) {
|
if (!data || !controls || !internalState) {
|
||||||
@@ -239,7 +301,7 @@ const CompactItemCard = ({
|
|||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||||
? (data as { userRating: null | number }).userRating
|
? (data as { userRating: null | number }).userRating
|
||||||
: null;
|
: null;
|
||||||
const hasRating = userRating !== null && userRating > 0;
|
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||||
|
|
||||||
const imageContainerClassName = clsx(styles.imageContainer, {
|
const imageContainerClassName = clsx(styles.imageContainer, {
|
||||||
[styles.isRound]: isRound,
|
[styles.isRound]: isRound,
|
||||||
@@ -247,21 +309,25 @@ const CompactItemCard = ({
|
|||||||
|
|
||||||
const imageContainerContent = (
|
const imageContainerContent = (
|
||||||
<>
|
<>
|
||||||
<Image
|
<ItemImage
|
||||||
className={clsx(styles.image, {
|
className={clsx(styles.image, {
|
||||||
[styles.isRound]: isRound,
|
[styles.isRound]: isRound,
|
||||||
})}
|
})}
|
||||||
src={imageUrl}
|
id={data?.id}
|
||||||
|
itemType={itemType}
|
||||||
|
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
||||||
/>
|
/>
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
{isFavorite && <div className={styles.favoriteBadge} />}
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{withControls && showControls && (
|
{withControls && showControls && data && (
|
||||||
<ItemCardControls
|
<ItemCardControls
|
||||||
controls={controls}
|
controls={controls}
|
||||||
enableExpansion={enableExpansion}
|
enableExpansion={enableExpansion}
|
||||||
|
internalState={internalState}
|
||||||
item={data}
|
item={data}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
|
showRating={hasRating}
|
||||||
type="compact"
|
type="compact"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -288,8 +354,10 @@ const CompactItemCard = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, styles.compact, {
|
className={clsx(styles.container, styles.compact, {
|
||||||
|
[styles.dragging]: isDragging,
|
||||||
[styles.selected]: isSelected,
|
[styles.selected]: isSelected,
|
||||||
})}
|
})}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
{enableNavigation && navigationPath && !internalState ? (
|
{enableNavigation && navigationPath && !internalState ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -351,11 +419,11 @@ const DefaultItemCard = ({
|
|||||||
data,
|
data,
|
||||||
enableExpansion,
|
enableExpansion,
|
||||||
enableNavigation,
|
enableNavigation,
|
||||||
imageUrl,
|
|
||||||
internalState,
|
internalState,
|
||||||
isRound,
|
isRound,
|
||||||
itemType,
|
itemType,
|
||||||
rows,
|
rows,
|
||||||
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
@@ -457,13 +525,15 @@ const DefaultItemCard = ({
|
|||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||||
? (data as { userRating: null | number }).userRating
|
? (data as { userRating: null | number }).userRating
|
||||||
: null;
|
: null;
|
||||||
const hasRating = userRating !== null && userRating > 0;
|
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||||
|
|
||||||
const imageContainerContent = (
|
const imageContainerContent = (
|
||||||
<>
|
<>
|
||||||
<Image
|
<ItemImage
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
||||||
src={imageUrl}
|
id={data?.id}
|
||||||
|
itemType={itemType}
|
||||||
|
src={(data as Album | AlbumArtist | Playlist | Song)?.imageUrl}
|
||||||
/>
|
/>
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
{isFavorite && <div className={styles.favoriteBadge} />}
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||||
@@ -474,6 +544,7 @@ const DefaultItemCard = ({
|
|||||||
enableExpansion={enableExpansion}
|
enableExpansion={enableExpansion}
|
||||||
item={data}
|
item={data}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
|
showRating={showRating}
|
||||||
type="default"
|
type="default"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -563,11 +634,11 @@ const PosterItemCard = ({
|
|||||||
enableDrag,
|
enableDrag,
|
||||||
enableExpansion,
|
enableExpansion,
|
||||||
enableNavigation,
|
enableNavigation,
|
||||||
imageUrl,
|
|
||||||
internalState,
|
internalState,
|
||||||
isRound,
|
isRound,
|
||||||
itemType,
|
itemType,
|
||||||
rows,
|
rows,
|
||||||
|
showRating,
|
||||||
withControls,
|
withControls,
|
||||||
}: ItemCardDerivativeProps) => {
|
}: ItemCardDerivativeProps) => {
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
@@ -716,13 +787,15 @@ const PosterItemCard = ({
|
|||||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||||
? (data as { userRating: null | number }).userRating
|
? (data as { userRating: null | number }).userRating
|
||||||
: null;
|
: null;
|
||||||
const hasRating = userRating !== null && userRating > 0;
|
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||||
|
|
||||||
const imageContainerContent = (
|
const imageContainerContent = (
|
||||||
<>
|
<>
|
||||||
<Image
|
<ItemImage
|
||||||
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
className={clsx(styles.image, { [styles.isRound]: isRound })}
|
||||||
src={imageUrl}
|
id={(data as { imageId: string })?.imageId}
|
||||||
|
itemType={itemType}
|
||||||
|
src={(data as { imageUrl: string })?.imageUrl}
|
||||||
/>
|
/>
|
||||||
{isFavorite && <div className={styles.favoriteBadge} />}
|
{isFavorite && <div className={styles.favoriteBadge} />}
|
||||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||||
@@ -734,6 +807,7 @@ const PosterItemCard = ({
|
|||||||
internalState={internalState}
|
internalState={internalState}
|
||||||
item={data}
|
item={data}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
|
showRating={showRating}
|
||||||
type="poster"
|
type="poster"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -925,7 +999,7 @@ export const getDataRows = (): DataRow[] => {
|
|||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
if ('duration' in data && data.duration !== null) {
|
if ('duration' in data && data.duration !== null) {
|
||||||
return formatDuration(data.duration * 1000);
|
return formatDurationString(data.duration);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@@ -943,7 +1017,7 @@ export const getDataRows = (): DataRow[] => {
|
|||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
if ('releaseDate' in data && data.releaseDate) {
|
if ('releaseDate' in data && data.releaseDate) {
|
||||||
return data.releaseDate;
|
return formatDateAbsoluteUTC(data.releaseDate);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@@ -961,7 +1035,12 @@ export const getDataRows = (): DataRow[] => {
|
|||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
if ('lastPlayedAt' in data && data.lastPlayedAt) {
|
if ('lastPlayedAt' in data && data.lastPlayedAt) {
|
||||||
return formatDateRelative(data.lastPlayedAt);
|
return (
|
||||||
|
<Group align="center" gap="xs">
|
||||||
|
<Icon icon="lastPlayed" size="sm" />
|
||||||
|
{formatDateRelative(data.lastPlayedAt)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@@ -970,7 +1049,7 @@ export const getDataRows = (): DataRow[] => {
|
|||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
if ('playCount' in data && data.playCount !== null) {
|
if ('playCount' in data && data.playCount !== null) {
|
||||||
return String(data.playCount);
|
return i18n.t('entity.play', { count: data.playCount });
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@@ -1019,7 +1098,7 @@ export const getDataRows = (): DataRow[] => {
|
|||||||
{
|
{
|
||||||
format: (data) => {
|
format: (data) => {
|
||||||
if ('songCount' in data && data.songCount !== null) {
|
if ('songCount' in data && data.songCount !== null) {
|
||||||
return String(data.songCount);
|
return i18n.t('entity.trackWithCount', { count: data.songCount });
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,146 +1,146 @@
|
|||||||
import { AnimatePresence } from 'motion/react';
|
// import { AnimatePresence } from 'motion/react';
|
||||||
import { MouseEvent, useMemo, useState } from 'react';
|
// import { MouseEvent, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router';
|
// import { Link } from 'react-router';
|
||||||
|
|
||||||
import styles from './item-detail.module.css';
|
// import styles from './item-detail.module.css';
|
||||||
|
|
||||||
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
// import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Badge } from '/@/shared/components/badge/badge';
|
// import { Badge } from '/@/shared/components/badge/badge';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
// import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
// import { Group } from '/@/shared/components/group/group';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
// import { Image } from '/@/shared/components/image/image';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
// import { Rating } from '/@/shared/components/rating/rating';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
// import { Text } from '/@/shared/components/text/text';
|
||||||
import {
|
// import {
|
||||||
Album,
|
// Album,
|
||||||
AlbumArtist,
|
// AlbumArtist,
|
||||||
Artist,
|
// Artist,
|
||||||
LibraryItem,
|
// LibraryItem,
|
||||||
Playlist,
|
// Playlist,
|
||||||
Song,
|
// Song,
|
||||||
} from '/@/shared/types/domain-types';
|
// } from '/@/shared/types/domain-types';
|
||||||
import { stringToColor } from '/@/shared/utils/string-to-color';
|
// import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||||
|
|
||||||
interface ItemDetailProps {
|
// interface ItemDetailProps {
|
||||||
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||||
itemHeight: number;
|
// itemHeight: number;
|
||||||
itemType: LibraryItem;
|
// itemType: LibraryItem;
|
||||||
onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||||
withControls?: boolean;
|
// withControls?: boolean;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
||||||
const imageUrl = getImageUrl(data);
|
// const imageUrl = getImageUrl(data);
|
||||||
|
|
||||||
const [showControls, setShowControls] = useState(false);
|
// const [showControls, setShowControls] = useState(false);
|
||||||
|
|
||||||
const { background } = useFastAverageColor({
|
// const { background } = useFastAverageColor({
|
||||||
algorithm: 'simple',
|
// algorithm: 'simple',
|
||||||
src: imageUrl,
|
// src: imageUrl,
|
||||||
srcLoaded: false,
|
// srcLoaded: false,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// const tags = [...(data?.genres ?? [])];
|
// // const tags = [...(data?.genres ?? [])];
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
// const tags = useMemo(() => {
|
||||||
if (!data) {
|
// if (!data) {
|
||||||
return [];
|
// return [];
|
||||||
}
|
// }
|
||||||
|
|
||||||
const items: {
|
// const items: {
|
||||||
color?: string;
|
// color?: string;
|
||||||
id: string;
|
// id: string;
|
||||||
isLight?: boolean;
|
// isLight?: boolean;
|
||||||
itemType: LibraryItem;
|
// itemType: LibraryItem;
|
||||||
name: string;
|
// name: string;
|
||||||
}[] = [];
|
// }[] = [];
|
||||||
|
|
||||||
if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
||||||
data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
// data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
||||||
items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if ('genres' in data && Array.isArray(data.genres)) {
|
// if ('genres' in data && Array.isArray(data.genres)) {
|
||||||
data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
||||||
const { color, isLight } = stringToColor(tag.name);
|
// const { color, isLight } = stringToColor(tag.name);
|
||||||
items.push({ ...tag, color, isLight });
|
// items.push({ ...tag, color, isLight });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// if ('tags' in data && typeof data.tags === 'object') {
|
// // if ('tags' in data && typeof data.tags === 'object') {
|
||||||
// console.log('data.tags :>> ', data.tags);
|
// // console.log('data.tags :>> ', data.tags);
|
||||||
// Object.entries(data.tags).forEach(([key, value]) => {
|
// // Object.entries(data.tags).forEach(([key, value]) => {
|
||||||
// items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
||||||
// });
|
// // });
|
||||||
// }
|
// // }
|
||||||
|
|
||||||
return items;
|
// return items;
|
||||||
}, [data]);
|
// }, [data]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div
|
// <div
|
||||||
className={styles.container}
|
// className={styles.container}
|
||||||
onClick={(e) => onClick?.(e, data, itemType)}
|
// onClick={(e) => onClick?.(e, data, itemType)}
|
||||||
style={{ backgroundColor: background }}
|
// style={{ backgroundColor: background }}
|
||||||
>
|
// >
|
||||||
<div
|
// <div
|
||||||
className={styles.imageContainer}
|
// className={styles.imageContainer}
|
||||||
onMouseEnter={() => withControls && setShowControls(true)}
|
// onMouseEnter={() => withControls && setShowControls(true)}
|
||||||
onMouseLeave={() => withControls && setShowControls(false)}
|
// onMouseLeave={() => withControls && setShowControls(false)}
|
||||||
>
|
// >
|
||||||
<Image alt={data?.name} src={imageUrl} />
|
// <Image alt={data?.name} src={imageUrl} />
|
||||||
<AnimatePresence>
|
// <AnimatePresence>
|
||||||
{withControls && showControls && <ItemCardControls type="compact" />}
|
// {withControls && showControls && <ItemCardControls type="compact" />}
|
||||||
</AnimatePresence>
|
// </AnimatePresence>
|
||||||
</div>
|
// </div>
|
||||||
<div className={styles.metadataContainer}>
|
// <div className={styles.metadataContainer}>
|
||||||
<div className={styles.header}>
|
// <div className={styles.header}>
|
||||||
<Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
||||||
{data?.name}
|
// {data?.name}
|
||||||
</Text>
|
// </Text>
|
||||||
<Group>
|
// <Group>
|
||||||
{data && 'userRating' in data && (
|
// {data && 'userRating' in data && (
|
||||||
<Rating size="xs" value={data?.userRating ?? 0} />
|
// <Rating size="xs" value={data?.userRating ?? 0} />
|
||||||
)}
|
// )}
|
||||||
{data && 'userFavorite' in data && (
|
// {data && 'userFavorite' in data && (
|
||||||
<ActionIcon
|
// <ActionIcon
|
||||||
icon="favorite"
|
// icon="favorite"
|
||||||
iconProps={{
|
// iconProps={{
|
||||||
fill: data?.userFavorite ? 'primary' : 'default',
|
// fill: data?.userFavorite ? 'primary' : 'default',
|
||||||
}}
|
// }}
|
||||||
size="xs"
|
// size="xs"
|
||||||
/>
|
// />
|
||||||
)}
|
// )}
|
||||||
</Group>
|
// </Group>
|
||||||
</div>
|
// </div>
|
||||||
<Divider />
|
// <Divider />
|
||||||
<div className={styles.content}>
|
// <div className={styles.content}>
|
||||||
<Group className={styles.tags} gap="xs">
|
// <Group className={styles.tags} gap="xs">
|
||||||
{tags.map((tag) => (
|
// {tags.map((tag) => (
|
||||||
<Badge
|
// <Badge
|
||||||
key={tag.id}
|
// key={tag.id}
|
||||||
style={{
|
// style={{
|
||||||
backgroundColor: tag.color,
|
// backgroundColor: tag.color,
|
||||||
color: tag.isLight ? 'black' : 'white',
|
// color: tag.isLight ? 'black' : 'white',
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
{tag.name}
|
// {tag.name}
|
||||||
</Badge>
|
// </Badge>
|
||||||
))}
|
// ))}
|
||||||
</Group>
|
// </Group>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
||||||
if (data && 'imageUrl' in data) {
|
// if (data && 'imageUrl' in data) {
|
||||||
return data.imageUrl || undefined;
|
// return data.imageUrl || undefined;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return undefined;
|
// return undefined;
|
||||||
};
|
// };
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import {
|
||||||
|
GeneralSettingsSchema,
|
||||||
|
useAuthStore,
|
||||||
|
useCurrentServerId,
|
||||||
|
useSettingsStore,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const getUnloaderIcon = (itemType: LibraryItem) => {
|
||||||
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
return 'emptyAlbumImage';
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return 'emptyArtistImage';
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return 'emptyArtistImage';
|
||||||
|
case LibraryItem.GENRE:
|
||||||
|
return 'emptyGenreImage';
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return 'emptyPlaylistImage';
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return 'emptySongImage';
|
||||||
|
default:
|
||||||
|
return 'emptyImage';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseItemImage = (
|
||||||
|
props: Omit<ImageProps, 'src'> & {
|
||||||
|
id?: null | string;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
src?: null | string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { src, ...rest } = props;
|
||||||
|
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: props.id,
|
||||||
|
imageUrl: src,
|
||||||
|
itemType: props.itemType,
|
||||||
|
size: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <BaseImage src={imageUrl} unloaderIcon={getUnloaderIcon(props.itemType)} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemImage = memo(BaseItemImage);
|
||||||
|
|
||||||
|
interface UseItemImageUrlProps {
|
||||||
|
id?: string;
|
||||||
|
imageUrl?: null | string;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
serverId?: string;
|
||||||
|
size?: number;
|
||||||
|
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useItemImageUrl = (args: UseItemImageUrlProps) => {
|
||||||
|
const { id, imageUrl, itemType, size, type } = args;
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
const imageRes = useSettingsStore((store) => store.general.imageRes);
|
||||||
|
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (imageUrl) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
api.controller.getImageUrl({
|
||||||
|
apiClientProps: { serverId: args.serverId || serverId },
|
||||||
|
query: { id, itemType, size: size ?? sizeByType },
|
||||||
|
}) || undefined
|
||||||
|
);
|
||||||
|
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getItemImageUrl(args: UseItemImageUrlProps) {
|
||||||
|
const { id, imageUrl, itemType, size, type } = args;
|
||||||
|
const authStore = useAuthStore.getState();
|
||||||
|
const currentServerId = authStore.currentServer?.id;
|
||||||
|
const serverId = (args.serverId || currentServerId) as string;
|
||||||
|
|
||||||
|
const imageRes = useSettingsStore.getState().general.imageRes;
|
||||||
|
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
api.controller.getImageUrl({
|
||||||
|
apiClientProps: { serverId },
|
||||||
|
query: { id, itemType, size: size ?? sizeByType },
|
||||||
|
}) || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import { eventEmitter } from '/@/renderer/events/event-emitter';
|
|||||||
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const getQueryKeyName = (itemType: LibraryItem): string => {
|
export const getListQueryKeyName = (itemType: LibraryItem): string => {
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
case LibraryItem.ALBUM:
|
case LibraryItem.ALBUM:
|
||||||
return 'albums';
|
return 'albums';
|
||||||
@@ -115,7 +115,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
|
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams),
|
||||||
});
|
});
|
||||||
|
|
||||||
const endIndex = startIndex + itemsPerPage;
|
const endIndex = startIndex + itemsPerPage;
|
||||||
@@ -186,10 +186,9 @@ export const useItemListInfiniteLoader = ({
|
|||||||
lastFetchedPageRef.current = -1;
|
lastFetchedPageRef.current = -1;
|
||||||
currentVisibleRangeRef.current = null;
|
currentVisibleRangeRef.current = null;
|
||||||
|
|
||||||
// Invalidate and wait for count query to refetch (this will suspend via useSuspenseQuery)
|
// Invalidate and wait for count query to refetch
|
||||||
await queryClient.refetchQueries({
|
await queryClient.ensureQueryData({
|
||||||
queryKey: countQueryKey,
|
queryKey: countQueryKey,
|
||||||
type: 'active',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch the first page after count is refetched
|
// Fetch the first page after count is refetched
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useIsFetching } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { getListQueryKeyName } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||||
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useIsFetchingItemListCount = ({ itemType }: { itemType: LibraryItem }) => {
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
const isFetching = useIsFetching({
|
||||||
|
queryKey: queryKeys[getListQueryKeyName(itemType)].count(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return isFetching > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsFetchingItemList = ({ itemType }: { itemType: LibraryItem }) => {
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
const isFetching = useIsFetching({
|
||||||
|
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return isFetching > 0;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
.item-grid-container {
|
.item-grid-container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ interface VirtualizedGridListProps {
|
|||||||
outerRef: RefObject<any>;
|
outerRef: RefObject<any>;
|
||||||
ref: RefObject<FixedSizeList<GridItemProps> | null>;
|
ref: RefObject<FixedSizeList<GridItemProps> | null>;
|
||||||
rows?: ItemCardProps['rows'];
|
rows?: ItemCardProps['rows'];
|
||||||
|
size?: 'compact' | 'default' | 'large';
|
||||||
tableMetaRef: RefObject<null | {
|
tableMetaRef: RefObject<null | {
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
@@ -95,6 +96,7 @@ const VirtualizedGridList = React.memo(
|
|||||||
outerRef,
|
outerRef,
|
||||||
ref,
|
ref,
|
||||||
rows,
|
rows,
|
||||||
|
size,
|
||||||
tableMetaRef,
|
tableMetaRef,
|
||||||
width,
|
width,
|
||||||
}: VirtualizedGridListProps) => {
|
}: VirtualizedGridListProps) => {
|
||||||
@@ -113,6 +115,7 @@ const VirtualizedGridList = React.memo(
|
|||||||
internalState,
|
internalState,
|
||||||
itemType,
|
itemType,
|
||||||
rows,
|
rows,
|
||||||
|
size,
|
||||||
tableMeta,
|
tableMeta,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -126,6 +129,7 @@ const VirtualizedGridList = React.memo(
|
|||||||
gap,
|
gap,
|
||||||
internalState,
|
internalState,
|
||||||
itemType,
|
itemType,
|
||||||
|
size,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleOnScroll = useCallback(
|
const handleOnScroll = useCallback(
|
||||||
@@ -215,7 +219,11 @@ const VirtualizedGridList = React.memo(
|
|||||||
|
|
||||||
VirtualizedGridList.displayName = 'VirtualizedGridList';
|
VirtualizedGridList.displayName = 'VirtualizedGridList';
|
||||||
|
|
||||||
const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) => {
|
const createThrottledSetTableMeta = (
|
||||||
|
itemsPerRow?: number,
|
||||||
|
rowsCount?: number,
|
||||||
|
size?: 'compact' | 'default' | 'large',
|
||||||
|
) => {
|
||||||
return throttle((width: number, dataLength: number, setTableMeta: (meta: any) => void) => {
|
return throttle((width: number, dataLength: number, setTableMeta: (meta: any) => void) => {
|
||||||
const isSm = width >= 600;
|
const isSm = width >= 600;
|
||||||
const isMd = width >= 768;
|
const isMd = width >= 768;
|
||||||
@@ -228,11 +236,11 @@ const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) =
|
|||||||
let dynamicItemsPerRow = 2;
|
let dynamicItemsPerRow = 2;
|
||||||
|
|
||||||
if (is4xl) {
|
if (is4xl) {
|
||||||
dynamicItemsPerRow = 12;
|
|
||||||
} else if (is3xl) {
|
|
||||||
dynamicItemsPerRow = 10;
|
dynamicItemsPerRow = 10;
|
||||||
} else if (is2xl) {
|
} else if (is3xl) {
|
||||||
dynamicItemsPerRow = 8;
|
dynamicItemsPerRow = 8;
|
||||||
|
} else if (is2xl) {
|
||||||
|
dynamicItemsPerRow = 7;
|
||||||
} else if (isXl) {
|
} else if (isXl) {
|
||||||
dynamicItemsPerRow = 6;
|
dynamicItemsPerRow = 6;
|
||||||
} else if (isLg) {
|
} else if (isLg) {
|
||||||
@@ -245,10 +253,22 @@ const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) =
|
|||||||
dynamicItemsPerRow = 2;
|
dynamicItemsPerRow = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (size === 'large') {
|
||||||
|
dynamicItemsPerRow = Math.round(dynamicItemsPerRow * 0.75);
|
||||||
|
if (dynamicItemsPerRow < 1) {
|
||||||
|
dynamicItemsPerRow = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
|
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
|
||||||
|
|
||||||
const widthPerItem = Number(width) / setItemsPerRow;
|
const widthPerItem = Number(width) / setItemsPerRow;
|
||||||
const itemHeight = widthPerItem + (rowsCount || getDataRowsCount()) * 26;
|
// For compact size, don't include text lines in height calculation
|
||||||
|
// CompactItemCard has a different layout that doesn't need the extra space
|
||||||
|
const itemHeight =
|
||||||
|
size === 'compact'
|
||||||
|
? widthPerItem
|
||||||
|
: widthPerItem + (rowsCount || getDataRowsCount()) * 26;
|
||||||
|
|
||||||
if (widthPerItem === 0) {
|
if (widthPerItem === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -273,6 +293,7 @@ export interface GridItemProps {
|
|||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
rows?: ItemCardProps['rows'];
|
rows?: ItemCardProps['rows'];
|
||||||
|
size?: 'compact' | 'default' | 'large';
|
||||||
tableMeta: null | {
|
tableMeta: null | {
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
@@ -286,6 +307,7 @@ export interface ItemGridListProps {
|
|||||||
enableDrag?: boolean;
|
enableDrag?: boolean;
|
||||||
enableExpansion?: boolean;
|
enableExpansion?: boolean;
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean;
|
||||||
|
enableSelectionDialog?: boolean;
|
||||||
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||||
getRowId?: ((item: unknown) => string) | string;
|
getRowId?: ((item: unknown) => string) | string;
|
||||||
initialTop?: {
|
initialTop?: {
|
||||||
@@ -300,6 +322,7 @@ export interface ItemGridListProps {
|
|||||||
overrideControls?: Partial<ItemControls>;
|
overrideControls?: Partial<ItemControls>;
|
||||||
ref?: Ref<ItemListHandle>;
|
ref?: Ref<ItemListHandle>;
|
||||||
rows?: ItemCardProps['rows'];
|
rows?: ItemCardProps['rows'];
|
||||||
|
size?: 'compact' | 'default' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseItemGridList = ({
|
const BaseItemGridList = ({
|
||||||
@@ -319,6 +342,7 @@ const BaseItemGridList = ({
|
|||||||
overrideControls,
|
overrideControls,
|
||||||
ref,
|
ref,
|
||||||
rows,
|
rows,
|
||||||
|
size = 'default',
|
||||||
}: ItemGridListProps) => {
|
}: ItemGridListProps) => {
|
||||||
const rootRef = useRef(null);
|
const rootRef = useRef(null);
|
||||||
const outerRef = useRef(null);
|
const outerRef = useRef(null);
|
||||||
@@ -409,8 +433,8 @@ const BaseItemGridList = ({
|
|||||||
}, [osInstance]);
|
}, [osInstance]);
|
||||||
|
|
||||||
const throttledSetTableMeta = useMemo(() => {
|
const throttledSetTableMeta = useMemo(() => {
|
||||||
return createThrottledSetTableMeta(itemsPerRow, rows?.length);
|
return createThrottledSetTableMeta(itemsPerRow, rows?.length, size);
|
||||||
}, [itemsPerRow, rows?.length]);
|
}, [itemsPerRow, rows?.length, size]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const { current: container } = containerRef;
|
const { current: container } = containerRef;
|
||||||
@@ -737,19 +761,23 @@ const BaseItemGridList = ({
|
|||||||
outerRef={outerRef}
|
outerRef={outerRef}
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
tableMetaRef={tableMetaRef}
|
tableMetaRef={tableMetaRef}
|
||||||
width={width}
|
width={width}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
<AnimatePresence presenceAffectsLayout>
|
||||||
|
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||||
|
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||||
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
||||||
const { index, style } = props;
|
const { index, style } = props;
|
||||||
const { columns, controls, data, enableDrag, gap, itemType, rows } = props.data;
|
const { columns, controls, data, enableDrag, gap, itemType, rows, size } = props.data;
|
||||||
|
|
||||||
const items: ReactNode[] = [];
|
const items: ReactNode[] = [];
|
||||||
const itemCount = data.length;
|
const itemCount = data.length;
|
||||||
@@ -780,6 +808,7 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
|||||||
internalState={props.data.internalState}
|
internalState={props.data.internalState}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
type={size === 'compact' ? 'compact' : 'poster'}
|
||||||
withControls
|
withControls
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import styles from './image-column.module.css';
|
import styles from './image-column.module.css';
|
||||||
|
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import {
|
import {
|
||||||
ItemTableListInnerColumn,
|
ItemTableListInnerColumn,
|
||||||
TableColumnContainer,
|
TableColumnContainer,
|
||||||
@@ -14,17 +15,14 @@ import {
|
|||||||
} from '/@/renderer/features/shared/components/play-button-group';
|
} from '/@/renderer/features/shared/components/play-button-group';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
import { Folder, LibraryItem } from '/@/shared/types/domain-types';
|
import { Folder, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const ImageColumn = (props: ItemTableListInnerColumn) => {
|
export const ImageColumn = (props: ItemTableListInnerColumn) => {
|
||||||
const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.[
|
const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id;
|
||||||
props.columns[props.columnIndex].id
|
|
||||||
];
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
const item = props.data[props.rowIndex] as any;
|
const item = props.data[props.rowIndex] as any;
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const internalState = (props as any).internalState;
|
const internalState = (props as any).internalState;
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -80,12 +78,14 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<Image
|
<ItemImage
|
||||||
containerClassName={clsx({
|
containerClassName={clsx({
|
||||||
[styles.imageContainerWithAspectRatio]:
|
[styles.imageContainerWithAspectRatio]:
|
||||||
props.size === 'default' || props.size === 'large',
|
props.size === 'default' || props.size === 'large',
|
||||||
})}
|
})}
|
||||||
src={row}
|
id={item?.id}
|
||||||
|
itemType={item?._itemType}
|
||||||
|
src={item?.imageUrl}
|
||||||
/>
|
/>
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
+19
-9
@@ -4,6 +4,7 @@ import { generatePath, Link } from 'react-router';
|
|||||||
|
|
||||||
import styles from './title-combined-column.module.css';
|
import styles from './title-combined-column.module.css';
|
||||||
|
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
||||||
import {
|
import {
|
||||||
ColumnNullFallback,
|
ColumnNullFallback,
|
||||||
@@ -19,13 +20,12 @@ import {
|
|||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types';
|
import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
||||||
const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex];
|
const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id;
|
||||||
const item = props.data[props.rowIndex] as any;
|
const item = props.data[props.rowIndex] as any;
|
||||||
const internalState = (props as any).internalState;
|
const internalState = (props as any).internalState;
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
@@ -74,8 +74,8 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const artists = useMemo(() => {
|
const artists = useMemo(() => {
|
||||||
if (row && 'artists' in row && Array.isArray(row.artists)) {
|
if (row && 'artists' in item && Array.isArray(item.artists)) {
|
||||||
return (row.artists as RelatedAlbumArtist[]).map((artist) => {
|
return (item.artists as RelatedAlbumArtist[]).map((artist) => {
|
||||||
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
||||||
artistId: artist.id,
|
artistId: artist.id,
|
||||||
});
|
});
|
||||||
@@ -83,9 +83,9 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [row]);
|
}, [item, row]);
|
||||||
|
|
||||||
if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) {
|
if (item && 'name' in item && 'imageUrl' in item && 'artists' in item) {
|
||||||
const rowHeight = props.getRowHeight(props.rowIndex, props);
|
const rowHeight = props.getRowHeight(props.rowIndex, props);
|
||||||
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
|
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
|
||||||
|
|
||||||
@@ -110,7 +110,12 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<Image containerClassName={styles.image} src={row.imageUrl as string} />
|
<ItemImage
|
||||||
|
containerClassName={styles.image}
|
||||||
|
id={item?.id}
|
||||||
|
itemType={item?._itemType}
|
||||||
|
src={item?.imageUrl}
|
||||||
|
/>
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.playButtonOverlay, {
|
className={clsx(styles.playButtonOverlay, {
|
||||||
@@ -138,7 +143,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
|
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
|
||||||
{row.name as string}
|
{item.name as string}
|
||||||
</Text>
|
</Text>
|
||||||
<div className={styles.artists}>
|
<div className={styles.artists}>
|
||||||
{artists.map((artist, index) => (
|
{artists.map((artist, index) => (
|
||||||
@@ -263,7 +268,12 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<Image containerClassName={styles.image} src={row.imageUrl as string} />
|
<ItemImage
|
||||||
|
containerClassName={styles.image}
|
||||||
|
id={item?.id}
|
||||||
|
itemType={item?._itemType}
|
||||||
|
src={item?.imageUrl}
|
||||||
|
/>
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.playButtonOverlay, {
|
className={clsx(styles.playButtonOverlay, {
|
||||||
|
|||||||
@@ -18,34 +18,34 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.ROW_INDEX,
|
value: TableColumn.ROW_INDEX,
|
||||||
width: 80,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.IMAGE,
|
value: TableColumn.IMAGE,
|
||||||
width: 70,
|
width: 70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.TITLE,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: false,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.TITLE_COMBINED,
|
value: TableColumn.TITLE_COMBINED,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: false,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ALBUM,
|
value: TableColumn.ALBUM,
|
||||||
@@ -70,7 +70,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ALBUM_ARTIST,
|
value: TableColumn.ALBUM_ARTIST,
|
||||||
@@ -115,7 +115,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.RELEASE_DATE,
|
value: TableColumn.RELEASE_DATE,
|
||||||
@@ -178,7 +178,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.LAST_PLAYED,
|
value: TableColumn.LAST_PLAYED,
|
||||||
@@ -214,7 +214,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.DATE_ADDED,
|
value: TableColumn.DATE_ADDED,
|
||||||
@@ -232,7 +232,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.PLAY_COUNT,
|
value: TableColumn.PLAY_COUNT,
|
||||||
@@ -252,7 +252,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.USER_FAVORITE,
|
value: TableColumn.USER_FAVORITE,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -268,9 +268,9 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.ACTIONS,
|
value: TableColumn.ACTIONS,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -284,34 +284,34 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.ROW_INDEX,
|
value: TableColumn.ROW_INDEX,
|
||||||
width: 80,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.IMAGE,
|
value: TableColumn.IMAGE,
|
||||||
width: 70,
|
width: 70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.TITLE,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: false,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||||
pinned: 'left',
|
pinned: null,
|
||||||
value: TableColumn.TITLE_COMBINED,
|
value: TableColumn.TITLE_COMBINED,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
@@ -327,7 +327,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ALBUM_ARTIST,
|
value: TableColumn.ALBUM_ARTIST,
|
||||||
@@ -381,7 +381,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.RELEASE_DATE,
|
value: TableColumn.RELEASE_DATE,
|
||||||
@@ -390,7 +390,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.LAST_PLAYED,
|
value: TableColumn.LAST_PLAYED,
|
||||||
@@ -399,7 +399,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.DATE_ADDED,
|
value: TableColumn.DATE_ADDED,
|
||||||
@@ -408,7 +408,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.PLAY_COUNT,
|
value: TableColumn.PLAY_COUNT,
|
||||||
@@ -419,7 +419,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.USER_FAVORITE,
|
value: TableColumn.USER_FAVORITE,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -435,9 +435,9 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.ACTIONS,
|
value: TableColumn.ACTIONS,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -451,7 +451,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ROW_INDEX,
|
value: TableColumn.ROW_INDEX,
|
||||||
width: 80,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -539,7 +539,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.USER_FAVORITE,
|
value: TableColumn.USER_FAVORITE,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -555,9 +555,9 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.ACTIONS,
|
value: TableColumn.ACTIONS,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -571,7 +571,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ROW_INDEX,
|
value: TableColumn.ROW_INDEX,
|
||||||
width: 80,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -630,9 +630,9 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.ACTIONS,
|
value: TableColumn.ACTIONS,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -646,7 +646,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.ROW_INDEX,
|
value: TableColumn.ROW_INDEX,
|
||||||
width: 80,
|
width: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
@@ -678,9 +678,9 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||||
pinned: 'right',
|
pinned: null,
|
||||||
value: TableColumn.ACTIONS,
|
value: TableColumn.ACTIONS,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
@@ -728,6 +728,15 @@ export const pickTableColumns = (options: {
|
|||||||
const enabledSet = new Set(enabledColumns);
|
const enabledSet = new Set(enabledColumns);
|
||||||
const remaining = columns.filter((col) => !enabledSet.has(col.value));
|
const remaining = columns.filter((col) => !enabledSet.has(col.value));
|
||||||
columnsToProcess = [...columnsToProcess, ...remaining];
|
columnsToProcess = [...columnsToProcess, ...remaining];
|
||||||
|
} else {
|
||||||
|
// When pickColumns is provided, include pickColumns that aren't in enabledColumns
|
||||||
|
// so they can be added as disabled entries
|
||||||
|
const enabledSet = new Set(enabledColumns);
|
||||||
|
const pickColumnsNotEnabled = pickColumns
|
||||||
|
.filter((col) => !enabledSet.has(col))
|
||||||
|
.map((col) => columnMap.get(col))
|
||||||
|
.filter((col): col is DefaultTableColumn => col !== undefined);
|
||||||
|
columnsToProcess = [...columnsToProcess, ...pickColumnsNotEnabled];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
columnsToProcess = columns;
|
columnsToProcess = columns;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.item-table-list-container {
|
.item-table-list-container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -672,6 +672,7 @@ interface ItemTableListProps {
|
|||||||
enableHorizontalBorders?: boolean;
|
enableHorizontalBorders?: boolean;
|
||||||
enableRowHoverHighlight?: boolean;
|
enableRowHoverHighlight?: boolean;
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean;
|
||||||
|
enableSelectionDialog?: boolean;
|
||||||
enableStickyGroupRows?: boolean;
|
enableStickyGroupRows?: boolean;
|
||||||
enableStickyHeader?: boolean;
|
enableStickyHeader?: boolean;
|
||||||
enableVerticalBorders?: boolean;
|
enableVerticalBorders?: boolean;
|
||||||
@@ -2318,6 +2319,7 @@ const BaseItemTableList = ({
|
|||||||
totalRowCount={totalRowCount}
|
totalRowCount={totalRowCount}
|
||||||
/>
|
/>
|
||||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||||
|
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.selection-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 320px;
|
||||||
|
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||||
|
color: var(--theme-colors-surface-foreground);
|
||||||
|
background: color-mix(in srgb, var(--theme-colors-surface) 85%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--theme-colors-border) 50%, transparent);
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
box-shadow:
|
||||||
|
2px 2px 10px 2px rgb(0 0 0 / 40%),
|
||||||
|
0 0 0 1px rgb(255 255 255 / 5%);
|
||||||
|
backdrop-filter: blur(12px) saturate(180%);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import styles from './selection-dialog.module.css';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import {
|
||||||
|
ItemListStateActions,
|
||||||
|
useItemListStateSubscription,
|
||||||
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Kbd } from '/@/shared/components/kbd/kbd';
|
||||||
|
import { Table } from '/@/shared/components/table/table';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
|
const controls = [
|
||||||
|
{
|
||||||
|
control1: <Kbd>CTRL</Kbd>,
|
||||||
|
control2: <Kbd>A</Kbd>,
|
||||||
|
label: i18n.t('action.selectAll', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control1: <Kbd>CTRL</Kbd>,
|
||||||
|
control2: <Icon fill="default" icon="mouseLeftClick" />,
|
||||||
|
label: i18n.t('action.addOrRemoveFromSelection', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control1: <Kbd>SHIFT</Kbd>,
|
||||||
|
control2: <Icon fill="default" icon="mouseLeftClick" />,
|
||||||
|
label: i18n.t('action.selectRangeOfItems', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SelectionDialog = ({ internalState }: { internalState: ItemListStateActions }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isListExpanded = useItemListStateSubscription(internalState, (state) =>
|
||||||
|
state ? state.expanded.size > 0 : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = useItemListStateSubscription(internalState, (state) =>
|
||||||
|
state ? state.selected.size : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
internalState.clearSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenMoreActions = (event: React.MouseEvent<unknown>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const selectedItems = internalState.getSelected();
|
||||||
|
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextMenuController.call({
|
||||||
|
cmd: { items: selectedItems as any[], type: (selectedItems[0] as any)._itemType },
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOpen = selectedCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={false} mode="sync">
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
{...animationProps.fadeIn}
|
||||||
|
className={styles.selectionIndicator}
|
||||||
|
style={{ bottom: isListExpanded ? '320px' : '1rem' }}
|
||||||
|
>
|
||||||
|
<Group gap="xl" justify="space-between">
|
||||||
|
<Group gap="sm">
|
||||||
|
<HoverCard offset={20} position="top">
|
||||||
|
<HoverCard.Target>
|
||||||
|
<span className={styles.infoIcon}>
|
||||||
|
<Icon icon="keyboard" />
|
||||||
|
</span>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown>
|
||||||
|
<Table>
|
||||||
|
<Table.Tbody>
|
||||||
|
{controls.map((control) => (
|
||||||
|
<Table.Tr key={control.label}>
|
||||||
|
<Table.Td ta="start">
|
||||||
|
{control.control1}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>+</Table.Td>
|
||||||
|
<Table.Td ta="center">
|
||||||
|
{control.control2}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="xs">{control.label}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
<Text fw={500} isNoSelect size="sm">
|
||||||
|
{t('common.countSelected', { count: selectedCount })}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
iconProps={{ size: 'xl' }}
|
||||||
|
onClick={handleClearSelection}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
icon="ellipsisHorizontal"
|
||||||
|
iconProps={{ size: 'xl' }}
|
||||||
|
onClick={handleOpenMoreActions}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -66,6 +66,7 @@ export interface ItemListComponentProps<TQuery> {
|
|||||||
export interface ItemListGridComponentProps<TQuery> extends ItemListComponentProps<TQuery> {
|
export interface ItemListGridComponentProps<TQuery> extends ItemListComponentProps<TQuery> {
|
||||||
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||||
itemsPerRow?: number;
|
itemsPerRow?: number;
|
||||||
|
size?: 'compact' | 'default' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemListHandle {
|
export interface ItemListHandle {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import styles from './native-scroll-area.module.css';
|
|||||||
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header/page-header';
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
|
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface NativeScrollAreaProps {
|
interface NativeScrollAreaProps {
|
||||||
@@ -26,35 +27,31 @@ const BaseNativeScrollArea = forwardRef(
|
|||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const scrollHandlerRef = useRef<null | number>(null);
|
const scrollHandler = useThrottledCallback((e: Event) => {
|
||||||
|
if (noHeader || !pageHeaderProps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollElement = e?.target as HTMLDivElement;
|
||||||
|
if (!scrollElement || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = pageHeaderProps.offset || 0;
|
||||||
|
const scrollTop = scrollElement.scrollTop;
|
||||||
|
|
||||||
|
if (scrollTop > offset) {
|
||||||
|
containerRef.current.setAttribute('data-scrolled', 'true');
|
||||||
|
} else {
|
||||||
|
containerRef.current.setAttribute('data-scrolled', 'false');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
const [initialize] = useOverlayScrollbars({
|
const [initialize] = useOverlayScrollbars({
|
||||||
defer: false,
|
defer: false,
|
||||||
events: {
|
events: {
|
||||||
scroll: (_instance, e) => {
|
scroll: (_instance, e) => {
|
||||||
if (scrollHandlerRef.current) {
|
scrollHandler(e);
|
||||||
cancelAnimationFrame(scrollHandlerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollHandlerRef.current = requestAnimationFrame(() => {
|
|
||||||
if (noHeader || !pageHeaderProps) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollElement = e?.target as HTMLDivElement;
|
|
||||||
if (!scrollElement || !containerRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = pageHeaderProps.offset || 0;
|
|
||||||
const scrollTop = scrollElement.scrollTop;
|
|
||||||
|
|
||||||
if (scrollTop > offset) {
|
|
||||||
containerRef.current.setAttribute('data-scrolled', 'true');
|
|
||||||
} else {
|
|
||||||
containerRef.current.setAttribute('data-scrolled', 'false');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -37,6 +37,18 @@
|
|||||||
input {
|
input {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[role='button'] {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
[style*='cursor: pointer'] {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header.pad-right {
|
.header.pad-right {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export type AutoDJQueueAddedEventPayload = {
|
||||||
|
songCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventMap = {
|
export type EventMap = {
|
||||||
|
AUTODJ_QUEUE_ADDED: AutoDJQueueAddedEventPayload;
|
||||||
ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
|
ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
|
||||||
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
|
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
|
||||||
MEDIA_NEXT: MediaNextEventPayload;
|
MEDIA_NEXT: MediaNextEventPayload;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const ServerRequired = () => {
|
|||||||
|
|
||||||
const isServerLock = Boolean(window.SERVER_LOCK) || false;
|
const isServerLock = Boolean(window.SERVER_LOCK) || false;
|
||||||
|
|
||||||
if (Object.keys(serverList).length > 1) {
|
if (Object.keys(serverList).length > 0) {
|
||||||
return (
|
return (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<Stack miw="300px">
|
<Stack miw="300px">
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const NoNetworkRoute = () => {
|
|||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<Center style={{ height: '100%', width: '100vw' }}>
|
<Center style={{ height: '100%' }}>
|
||||||
<Stack gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
|
<Stack align="center" gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
|
||||||
<Icon icon="wifiOff" size="4rem" />
|
<Icon icon="wifiOff" size="4rem" />
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="xl" weight={600}>
|
<Text size="xl" weight={600}>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
|||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
@@ -88,6 +89,12 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
|||||||
items.push(...releaseTypes);
|
items.push(...releaseTypes);
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
|
{
|
||||||
|
id: 'isCompilation',
|
||||||
|
value: album?.isCompilation
|
||||||
|
? t('filter.isCompilation', { postProcess: 'sentenceCase' })
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'releaseDate',
|
id: 'releaseDate',
|
||||||
value: album.releaseDate
|
value: album.releaseDate
|
||||||
@@ -96,7 +103,11 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'releaseYear',
|
id: 'releaseYear',
|
||||||
value: album.releaseYear?.toString(),
|
value: album.releaseDate
|
||||||
|
? undefined
|
||||||
|
: album.releaseYear
|
||||||
|
? album.releaseYear.toString()
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'songCount',
|
id: 'songCount',
|
||||||
@@ -136,19 +147,7 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
|||||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'isCompilation',
|
|
||||||
value: album?.isCompilation
|
|
||||||
? t('filter.isCompilation', { postProcess: 'sentenceCase' })
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recordLabels',
|
|
||||||
value:
|
|
||||||
album.recordLabels && album.recordLabels.length > 0
|
|
||||||
? album.recordLabels.join(', ')
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'version',
|
id: 'version',
|
||||||
value: album.version || undefined,
|
value: album.version || undefined,
|
||||||
@@ -342,6 +341,7 @@ export const AlbumDetailContent = () => {
|
|||||||
uniqueId: 'moreFromArtist',
|
uniqueId: 'moreFromArtist',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
enableRefresh: true,
|
||||||
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
|
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
|
||||||
isHidden: !detailQuery?.data?.genres?.[0],
|
isHidden: !detailQuery?.data?.genres?.[0],
|
||||||
query: {
|
query: {
|
||||||
@@ -362,6 +362,9 @@ export const AlbumDetailContent = () => {
|
|||||||
|
|
||||||
const comment = detailQuery?.data?.comment;
|
const comment = detailQuery?.data?.comment;
|
||||||
|
|
||||||
|
const releaseYear = detailQuery?.data?.releaseYear;
|
||||||
|
const labels = detailQuery?.data?.recordLabels;
|
||||||
|
|
||||||
const mbzId = detailQuery?.data?.mbzId;
|
const mbzId = detailQuery?.data?.mbzId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -369,9 +372,7 @@ export const AlbumDetailContent = () => {
|
|||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
{comment && (
|
{comment && (
|
||||||
<Spoiler maxHeight={75}>
|
<Spoiler maxHeight={75}>
|
||||||
<Text
|
<Text pb="md">{replaceURLWithHTMLLinks(comment)}</Text>
|
||||||
dangerouslySetInnerHTML={{ __html: replaceURLWithHTMLLinks(comment) }}
|
|
||||||
/>
|
|
||||||
</Spoiler>
|
</Spoiler>
|
||||||
)}
|
)}
|
||||||
<div className={styles.contentLayout}>
|
<div className={styles.contentLayout}>
|
||||||
@@ -396,7 +397,15 @@ export const AlbumDetailContent = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{labels && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
{labels.map((label) => (
|
||||||
|
<Text isMuted key={`label-${label}`} size="sm">
|
||||||
|
℗{releaseYear ? ` ${releaseYear}` : ''} {label}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
<Stack gap="lg" mt="3rem">
|
<Stack gap="lg" mt="3rem">
|
||||||
{cq.height || cq.width ? (
|
{cq.height || cq.width ? (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
@@ -404,6 +413,7 @@ export const AlbumDetailContent = () => {
|
|||||||
.filter((c) => !c.isHidden)
|
.filter((c) => !c.isHidden)
|
||||||
.map((carousel) => (
|
.map((carousel) => (
|
||||||
<AlbumInfiniteCarousel
|
<AlbumInfiniteCarousel
|
||||||
|
enableRefresh={carousel.enableRefresh}
|
||||||
excludeIds={carousel.excludeIds}
|
excludeIds={carousel.excludeIds}
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
query={carousel.query}
|
query={carousel.query}
|
||||||
@@ -428,6 +438,7 @@ interface AlbumDetailSongsTableProps {
|
|||||||
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table);
|
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table);
|
||||||
|
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
@@ -441,11 +452,11 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
|||||||
|
|
||||||
const filteredSongs = useMemo(() => {
|
const filteredSongs = useMemo(() => {
|
||||||
return sortSongList(
|
return sortSongList(
|
||||||
searchLibraryItems(songs, searchTerm, LibraryItem.SONG),
|
searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG),
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
);
|
);
|
||||||
}, [songs, searchTerm, sortBy, sortOrder]);
|
}, [songs, debouncedSearchTerm, sortBy, sortOrder]);
|
||||||
|
|
||||||
const { handleColumnReordered } = useItemListColumnReorder({
|
const { handleColumnReordered } = useItemListColumnReorder({
|
||||||
itemListKey: ItemListKey.ALBUM_DETAIL,
|
itemListKey: ItemListKey.ALBUM_DETAIL,
|
||||||
@@ -493,7 +504,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
|||||||
|
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
// Remove groups when filtering
|
// Remove groups when filtering
|
||||||
if (searchTerm.trim()) {
|
if (debouncedSearchTerm.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +590,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
|||||||
},
|
},
|
||||||
rowHeight: 40,
|
rowHeight: 40,
|
||||||
}));
|
}));
|
||||||
}, [searchTerm, sortBy, discGroups, t]);
|
}, [debouncedSearchTerm, sortBy, discGroups, t]);
|
||||||
|
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
|
|
||||||
@@ -677,6 +688,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
|||||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||||
enableSelection
|
enableSelection
|
||||||
|
enableSelectionDialog={false}
|
||||||
enableStickyGroupRows
|
enableStickyGroupRows
|
||||||
enableStickyHeader
|
enableStickyHeader
|
||||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { generatePath, Link, useParams } from 'react-router';
|
|||||||
|
|
||||||
import styles from './album-detail-header.module.css';
|
import styles from './album-detail-header.module.css';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
LibraryHeaderMenu,
|
LibraryHeaderMenu,
|
||||||
} from '/@/renderer/features/shared/components/library-header';
|
} from '/@/renderer/features/shared/components/library-header';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
@@ -23,13 +24,15 @@ import { Play } from '/@/shared/types/types';
|
|||||||
export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const { showRatings } = useGeneralSettings();
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useQuery(
|
||||||
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
|
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showRating =
|
const showRating =
|
||||||
detailQuery?.data?._serverType === ServerType.NAVIDROME ||
|
showRatings &&
|
||||||
detailQuery?.data?._serverType === ServerType.SUBSONIC;
|
(detailQuery?.data?._serverType === ServerType.NAVIDROME ||
|
||||||
|
detailQuery?.data?._serverType === ServerType.SUBSONIC);
|
||||||
|
|
||||||
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
@@ -82,10 +85,16 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
|||||||
const firstAlbumArtist = detailQuery?.data?.albumArtists?.[0];
|
const firstAlbumArtist = detailQuery?.data?.albumArtists?.[0];
|
||||||
const releaseYear = detailQuery?.data?.releaseYear;
|
const releaseYear = detailQuery?.data?.releaseYear;
|
||||||
|
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: detailQuery?.data?.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
type: 'header',
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack ref={ref}>
|
<Stack ref={ref}>
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
imageUrl={detailQuery?.data?.imageUrl}
|
imageUrl={imageUrl}
|
||||||
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
|
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery?.data?.name || ''}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumCarouselProps {
|
interface AlbumCarouselProps {
|
||||||
|
enableRefresh?: boolean;
|
||||||
excludeIds?: string[];
|
excludeIds?: string[];
|
||||||
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
||||||
rowCount?: number;
|
rowCount?: number;
|
||||||
@@ -28,7 +29,15 @@ interface AlbumCarouselProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
||||||
const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
|
const {
|
||||||
|
enableRefresh,
|
||||||
|
excludeIds,
|
||||||
|
query: additionalQuery,
|
||||||
|
rowCount = 1,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
title,
|
||||||
|
} = props;
|
||||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||||
const {
|
const {
|
||||||
data: albums,
|
data: albums,
|
||||||
@@ -81,6 +90,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
return (
|
return (
|
||||||
<GridCarousel
|
<GridCarousel
|
||||||
cards={cards}
|
cards={cards}
|
||||||
|
enableRefresh={enableRefresh}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
loadNextPage={fetchNextPage}
|
loadNextPage={fetchNextPage}
|
||||||
onNextPage={handleNextPage}
|
onNextPage={handleNextPage}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export const AlbumListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -126,6 +127,7 @@ export const AlbumListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { Suspense, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useIsFetchingItemListCount } from '/@/renderer/components/item-list/helpers/use-is-fetching-item-list';
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||||
@@ -22,17 +23,13 @@ interface AlbumListHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
|
export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
|
||||||
const { itemCount } = useListContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<LibraryHeaderBar ignoreMaxWidth>
|
<LibraryHeaderBar ignoreMaxWidth>
|
||||||
<PlayButton />
|
<PlayButton />
|
||||||
<PageTitle title={title} />
|
<PageTitle title={title} />
|
||||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
<AlbumListHeaderBadge />
|
||||||
{itemCount}
|
|
||||||
</LibraryHeaderBar.Badge>
|
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
<Group>
|
<Group>
|
||||||
<ListSearchInput />
|
<ListSearchInput />
|
||||||
@@ -45,6 +42,16 @@ export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AlbumListHeaderBadge = () => {
|
||||||
|
const { itemCount } = useListContext();
|
||||||
|
|
||||||
|
const isFetching = useIsFetchingItemListCount({
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <LibraryHeaderBar.Badge isLoading={isFetching}>{itemCount}</LibraryHeaderBar.Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
const PageTitle = ({ title }: { title?: string }) => {
|
const PageTitle = ({ title }: { title?: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pageKey } = useListContext();
|
const { pageKey } = useListContext();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const AlbumListInfiniteGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: AlbumListInfiniteGridProps) => {
|
}: AlbumListInfiniteGridProps) => {
|
||||||
const listCountQuery = albumQueries.listCount({
|
const listCountQuery = albumQueries.listCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -65,6 +66,7 @@ export const AlbumListInfiniteGrid = ({
|
|||||||
onRangeChanged={onRangeChanged}
|
onRangeChanged={onRangeChanged}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const AlbumListPaginatedGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: AlbumListPaginatedGridProps) => {
|
}: AlbumListPaginatedGridProps) => {
|
||||||
const listCountQuery = albumQueries.listCount({
|
const listCountQuery = albumQueries.listCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -77,6 +78,7 @@ export const AlbumListPaginatedGrid = ({
|
|||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
</ItemListWithPagination>
|
</ItemListWithPagination>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Fragment, Suspense, useCallback, useRef } from 'react';
|
|||||||
|
|
||||||
import styles from './expanded-album-list-item.module.css';
|
import styles from './expanded-album-list-item.module.css';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
||||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import {
|
import {
|
||||||
@@ -197,10 +198,16 @@ export const ExpandedAlbumListItem = ({ internalState, item }: ExpandedAlbumList
|
|||||||
|
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
|
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: item.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
type: 'itemCard',
|
||||||
|
});
|
||||||
|
|
||||||
const color = useFastAverageColor({
|
const color = useFastAverageColor({
|
||||||
algorithm: 'sqrt',
|
algorithm: 'sqrt',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
src: data?.imageUrl,
|
src: imageUrl,
|
||||||
srcLoaded: true,
|
srcLoaded: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,7 +307,7 @@ export const ExpandedAlbumListItem = ({ internalState, item }: ExpandedAlbumList
|
|||||||
className={styles.backgroundImage}
|
className={styles.backgroundImage}
|
||||||
style={{
|
style={{
|
||||||
['--bg-color' as string]: color?.background,
|
['--bg-color' as string]: color?.background,
|
||||||
backgroundImage: `url(${data?.imageUrl})`,
|
backgroundImage: `url(${imageUrl})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{data?.songs && data.songs.length > 0 && (
|
{data?.songs && data.songs.length > 0 && (
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { ChangeEvent, useMemo } from 'react';
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
MultiSelectWithInvalidData,
|
|
||||||
SelectWithInvalidData,
|
|
||||||
} from '/@/renderer/components/select-with-invalid-data';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
@@ -187,14 +184,14 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
|||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectableAlbumArtists}
|
data={selectableAlbumArtists}
|
||||||
defaultValue={query.artistIds?.[0] || undefined}
|
defaultValue={query.artistIds || []}
|
||||||
disabled={disableArtistFilter}
|
disabled={disableArtistFilter}
|
||||||
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
limit={300}
|
limit={300}
|
||||||
onChange={(e) => setAlbumArtist(e ? [e] : null)}
|
onChange={(e) => (e && e.length > 0 ? setAlbumArtist(e) : setAlbumArtist(null))}
|
||||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
@@ -206,10 +203,10 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
|||||||
|
|
||||||
interface TagFilterItemProps {
|
interface TagFilterItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
onChange: (value: null | string) => void;
|
onChange: (value: null | string[]) => void;
|
||||||
options: Array<{ id: string; name: string }>;
|
options: Array<{ id: string; name: string }>;
|
||||||
tagValue: string;
|
tagValue: string;
|
||||||
value: string | undefined;
|
value: string | string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterItemProps) => {
|
const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterItemProps) => {
|
||||||
@@ -222,15 +219,20 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
|||||||
[options],
|
[options],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultValue = useMemo(() => {
|
||||||
|
if (!value) return [];
|
||||||
|
return Array.isArray(value) ? value : [value];
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectData}
|
data={selectData}
|
||||||
defaultValue={value}
|
defaultValue={defaultValue}
|
||||||
key={tagValue}
|
key={tagValue}
|
||||||
label={label}
|
label={label}
|
||||||
limit={100}
|
limit={100}
|
||||||
onChange={onChange}
|
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -257,7 +259,7 @@ const TagFilters = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleTagFilter = useMemo(
|
const handleTagFilter = useMemo(
|
||||||
() => (tag: string, e: null | string) => {
|
() => (tag: string, e: null | string[]) => {
|
||||||
setCustom({ [tag]: e });
|
setCustom({ [tag]: e });
|
||||||
},
|
},
|
||||||
[setCustom],
|
[setCustom],
|
||||||
@@ -289,7 +291,7 @@ const TagFilters = () => {
|
|||||||
onChange={(e) => handleTagFilter(tag.value, e)}
|
onChange={(e) => handleTagFilter(tag.value, e)}
|
||||||
options={tag.options}
|
options={tag.options}
|
||||||
tagValue={tag.value}
|
tagValue={tag.value}
|
||||||
value={query._custom?.[tag.value] as string | undefined}
|
value={query._custom?.[tag.value] as string | string[] | undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
|
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
|
||||||
@@ -34,9 +35,16 @@ const AlbumDetailRoute = () => {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const imageUrl =
|
||||||
|
useItemImageUrl({
|
||||||
|
id: detailQuery?.data?.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
type: 'itemCard',
|
||||||
|
}) || '';
|
||||||
|
|
||||||
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
|
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
|
||||||
id: albumId,
|
id: albumId,
|
||||||
src: detailQuery.data?.imageUrl,
|
src: imageUrl,
|
||||||
srcLoaded: !detailQuery.isLoading,
|
srcLoaded: !detailQuery.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,7 +53,7 @@ const AlbumDetailRoute = () => {
|
|||||||
const showBlurredImage = albumBackground;
|
const showBlurredImage = albumBackground;
|
||||||
|
|
||||||
const { isReady } = useWaitForColorCalculation({
|
const { isReady } = useWaitForColorCalculation({
|
||||||
hasImage: !!detailQuery.data?.imageUrl,
|
hasImage: !!imageUrl,
|
||||||
isLoading: isColorLoading,
|
isLoading: isColorLoading,
|
||||||
routeId: albumId,
|
routeId: albumId,
|
||||||
showBlurredImage,
|
showBlurredImage,
|
||||||
@@ -81,7 +89,7 @@ const AlbumDetailRoute = () => {
|
|||||||
<LibraryBackgroundImage
|
<LibraryBackgroundImage
|
||||||
blur={albumBackgroundBlur}
|
blur={albumBackgroundBlur}
|
||||||
headerRef={headerRef}
|
headerRef={headerRef}
|
||||||
imageUrl={detailQuery.data?.imageUrl}
|
imageUrl={imageUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import styles from './dummy-album-detail-route.module.css';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||||
@@ -113,12 +114,18 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: detailQuery?.data?.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
type: 'header',
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`dummy-album-detail-${albumId}`}>
|
<AnimatedPage key={`dummy-album-detail-${albumId}`}>
|
||||||
<LibraryContainer>
|
<LibraryContainer>
|
||||||
<Stack>
|
<Stack>
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
imageUrl={detailQuery?.data?.imageUrl}
|
imageUrl={imageUrl}
|
||||||
item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }}
|
item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }}
|
||||||
loading={!background || colorId !== albumId}
|
loading={!background || colorId !== albumId}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery?.data?.name || ''}
|
||||||
@@ -212,11 +219,7 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
{comment && (
|
{comment && (
|
||||||
<section>
|
<section>
|
||||||
<Spoiler maxHeight={75}>
|
<Spoiler maxHeight={75}>
|
||||||
<Text
|
<Text pb="md">{replaceURLWithHTMLLinks(comment)}</Text>
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: replaceURLWithHTMLLinks(comment),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Spoiler>
|
</Spoiler>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,3 +10,57 @@
|
|||||||
gap: var(--theme-spacing-2xl);
|
gap: var(--theme-spacing-2xl);
|
||||||
padding: 1rem 2rem 5rem;
|
padding: 1rem 2rem 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.album-section-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--theme-spacing-4xl);
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-section-title {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--theme-spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-section-divider-container {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-section-divider {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--theme-colors-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.similar-artists-title {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-grid-item {
|
||||||
|
flex: 1 1
|
||||||
|
calc((100% - (var(--items-per-row) - 1) * var(--theme-spacing-md)) / var(--items-per-row));
|
||||||
|
min-width: 0;
|
||||||
|
max-width: calc(
|
||||||
|
(100% - (var(--items-per-row) - 1) * var(--theme-spacing-md)) / var(--items-per-row)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { forwardRef, Fragment, Ref } from 'react';
|
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import styles from './album-artist-detail-header.module.css';
|
import styles from './album-artist-detail-header.module.css';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
LibraryHeaderMenu,
|
LibraryHeaderMenu,
|
||||||
} from '/@/renderer/features/shared/components/library-header';
|
} from '/@/renderer/features/shared/components/library-header';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
@@ -29,17 +30,18 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
|||||||
};
|
};
|
||||||
const routeId = (artistId || albumArtistId) as string;
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const { showRatings } = useGeneralSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useSuspenseQuery(
|
||||||
artistsQueries.albumArtistDetail({
|
artistsQueries.albumArtistDetail({
|
||||||
query: { id: routeId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const albumCount = detailQuery?.data?.albumCount;
|
const albumCount = detailQuery.data?.albumCount;
|
||||||
const songCount = detailQuery?.data?.songCount;
|
const songCount = detailQuery.data?.songCount;
|
||||||
const duration = detailQuery?.data?.duration;
|
const duration = detailQuery.data?.duration;
|
||||||
const durationEnabled = duration !== null && duration !== undefined;
|
const durationEnabled = duration !== null && duration !== undefined;
|
||||||
|
|
||||||
const metadataItems = [
|
const metadataItems = [
|
||||||
@@ -66,62 +68,82 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
|||||||
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const handlePlay = (type?: Play) => {
|
const handlePlay = useCallback(
|
||||||
if (!server?.id || !routeId) return;
|
(type?: Play) => {
|
||||||
addToQueueByFetch(
|
if (!server?.id || !routeId) return;
|
||||||
server.id,
|
addToQueueByFetch(
|
||||||
[routeId],
|
server.id,
|
||||||
LibraryItem.ALBUM_ARTIST,
|
[routeId],
|
||||||
type || playButtonBehavior,
|
LibraryItem.ALBUM_ARTIST,
|
||||||
);
|
type || playButtonBehavior,
|
||||||
};
|
);
|
||||||
|
},
|
||||||
|
[addToQueueByFetch, playButtonBehavior, routeId, server.id],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFavorite = () => {
|
const handleFavorite = useCallback(() => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery.data) return;
|
||||||
setFavorite(
|
setFavorite(
|
||||||
detailQuery.data._serverId,
|
detailQuery.data._serverId,
|
||||||
[detailQuery.data.id],
|
[detailQuery.data.id],
|
||||||
LibraryItem.ALBUM_ARTIST,
|
LibraryItem.ALBUM_ARTIST,
|
||||||
!detailQuery.data.userFavorite,
|
!detailQuery.data.userFavorite,
|
||||||
);
|
);
|
||||||
};
|
}, [detailQuery.data, setFavorite]);
|
||||||
|
|
||||||
const handleUpdateRating = (rating: number) => {
|
const handleUpdateRating = useCallback(
|
||||||
if (!detailQuery?.data) return;
|
(rating: number) => {
|
||||||
|
if (!detailQuery.data) return;
|
||||||
|
|
||||||
|
if (detailQuery.data.userRating === rating) {
|
||||||
|
return setRating(
|
||||||
|
detailQuery.data._serverId,
|
||||||
|
[detailQuery.data.id],
|
||||||
|
LibraryItem.ALBUM_ARTIST,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (detailQuery.data.userRating === rating) {
|
|
||||||
return setRating(
|
return setRating(
|
||||||
detailQuery.data._serverId,
|
detailQuery.data._serverId,
|
||||||
[detailQuery.data.id],
|
[detailQuery.data.id],
|
||||||
LibraryItem.ALBUM_ARTIST,
|
LibraryItem.ALBUM_ARTIST,
|
||||||
0,
|
rating,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
[detailQuery.data, setRating],
|
||||||
|
);
|
||||||
|
|
||||||
return setRating(
|
const handleMoreOptions = useCallback(
|
||||||
detailQuery.data._serverId,
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
[detailQuery.data.id],
|
if (!detailQuery.data) return;
|
||||||
LibraryItem.ALBUM_ARTIST,
|
ContextMenuController.call({
|
||||||
rating,
|
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
||||||
);
|
event: e,
|
||||||
};
|
});
|
||||||
|
},
|
||||||
|
[detailQuery.data],
|
||||||
|
);
|
||||||
|
|
||||||
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const imageUrl = useItemImageUrl({
|
||||||
if (!detailQuery?.data) return;
|
id: detailQuery.data?.imageId || undefined,
|
||||||
ContextMenuController.call({
|
imageUrl: detailQuery.data?.imageUrl,
|
||||||
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
event: e,
|
type: 'itemCard',
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
|
const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME;
|
||||||
|
|
||||||
|
const selectedImageUrl = useMemo(() => {
|
||||||
|
return detailQuery.data?.imageUrl || imageUrl;
|
||||||
|
}, [detailQuery.data?.imageUrl, imageUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
imageUrl={detailQuery?.data?.imageUrl}
|
imageUrl={selectedImageUrl}
|
||||||
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
|
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery.data?.name || ''}
|
||||||
>
|
>
|
||||||
<Stack gap="md" w="100%">
|
<Stack gap="md" w="100%">
|
||||||
<Group className={styles.metadataGroup}>
|
<Group className={styles.metadataGroup}>
|
||||||
@@ -135,13 +157,13 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
|||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
<LibraryHeaderMenu
|
<LibraryHeaderMenu
|
||||||
favorite={detailQuery?.data?.userFavorite}
|
favorite={detailQuery.data?.userFavorite}
|
||||||
onFavorite={handleFavorite}
|
onFavorite={handleFavorite}
|
||||||
onMore={handleMoreOptions}
|
onMore={handleMoreOptions}
|
||||||
onPlay={(type) => handlePlay(type)}
|
onPlay={(type) => handlePlay(type)}
|
||||||
onRating={showRating ? handleUpdateRating : undefined}
|
onRating={showRating ? handleUpdateRating : undefined}
|
||||||
onShuffle={() => handlePlay(Play.SHUFFLE)}
|
onShuffle={() => handlePlay(Play.SHUFFLE)}
|
||||||
rating={detailQuery?.data?.userRating || 0}
|
rating={detailQuery.data?.userRating || 0}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</LibraryHeader>
|
</LibraryHeader>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
|
|||||||
controls={controls}
|
controls={controls}
|
||||||
data={albumArtist}
|
data={albumArtist}
|
||||||
enableDrag
|
enableDrag
|
||||||
|
isRound
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
type="poster"
|
type="poster"
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const AlbumArtistListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,6 +106,7 @@ export const AlbumArtistListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useIsFetchingItemListCount } from '/@/renderer/components/item-list/helpers/use-is-fetching-item-list';
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||||
@@ -18,7 +19,6 @@ interface AlbumArtistListHeaderProps {
|
|||||||
export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) => {
|
export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { itemCount } = useListContext();
|
|
||||||
const pageTitle = title || t('page.albumArtistList.title', { postProcess: 'titleCase' });
|
const pageTitle = title || t('page.albumArtistList.title', { postProcess: 'titleCase' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,9 +27,7 @@ export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) =>
|
|||||||
<LibraryHeaderBar ignoreMaxWidth>
|
<LibraryHeaderBar ignoreMaxWidth>
|
||||||
<PlayButton />
|
<PlayButton />
|
||||||
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
<AlbumArtistListHeaderBadge />
|
||||||
{itemCount}
|
|
||||||
</LibraryHeaderBar.Badge>
|
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
<Group>
|
<Group>
|
||||||
<ListSearchInput />
|
<ListSearchInput />
|
||||||
@@ -42,6 +40,16 @@ export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AlbumArtistListHeaderBadge = () => {
|
||||||
|
const { itemCount } = useListContext();
|
||||||
|
|
||||||
|
const isFetching = useIsFetchingItemListCount({
|
||||||
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <LibraryHeaderBar.Badge isLoading={isFetching}>{itemCount}</LibraryHeaderBar.Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
const PlayButton = () => {
|
const PlayButton = () => {
|
||||||
const { query } = useAlbumArtistListFilters();
|
const { query } = useAlbumArtistListFilters();
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const AlbumArtistListInfiniteGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: AlbumArtistListInfiniteGridProps) => {
|
}: AlbumArtistListInfiniteGridProps) => {
|
||||||
const listCountQuery = artistsQueries.albumArtistListCount({
|
const listCountQuery = artistsQueries.albumArtistListCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -65,6 +66,7 @@ export const AlbumArtistListInfiniteGrid = ({
|
|||||||
onRangeChanged={onRangeChanged}
|
onRangeChanged={onRangeChanged}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const AlbumArtistListPaginatedGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: AlbumArtistListPaginatedGridProps) => {
|
}: AlbumArtistListPaginatedGridProps) => {
|
||||||
const listCountQuery = artistsQueries.albumArtistListCount({
|
const listCountQuery = artistsQueries.albumArtistListCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -77,6 +78,7 @@ export const AlbumArtistListPaginatedGrid = ({
|
|||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
</ItemListWithPagination>
|
</ItemListWithPagination>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const ArtistListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,6 +98,7 @@ export const ArtistListView = ({
|
|||||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
query={mergedQuery}
|
query={mergedQuery}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
|
size={grid.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useIsFetchingItemListCount } from '/@/renderer/components/item-list/helpers/use-is-fetching-item-list';
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
|
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
|
||||||
@@ -18,7 +19,6 @@ interface ArtistListHeaderProps {
|
|||||||
export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { itemCount } = useListContext();
|
|
||||||
const pageTitle = title || t('entity.artist_other', { postProcess: 'titleCase' });
|
const pageTitle = title || t('entity.artist_other', { postProcess: 'titleCase' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,9 +27,7 @@ export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
|||||||
<LibraryHeaderBar ignoreMaxWidth>
|
<LibraryHeaderBar ignoreMaxWidth>
|
||||||
<PlayButton />
|
<PlayButton />
|
||||||
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
<ArtistListHeaderBadge />
|
||||||
{itemCount}
|
|
||||||
</LibraryHeaderBar.Badge>
|
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
<Group>
|
<Group>
|
||||||
<ListSearchInput />
|
<ListSearchInput />
|
||||||
@@ -42,6 +40,16 @@ export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ArtistListHeaderBadge = () => {
|
||||||
|
const { itemCount } = useListContext();
|
||||||
|
|
||||||
|
const isFetching = useIsFetchingItemListCount({
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <LibraryHeaderBar.Badge isLoading={isFetching}>{itemCount}</LibraryHeaderBar.Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
const PlayButton = () => {
|
const PlayButton = () => {
|
||||||
const { query } = useArtistListFilters();
|
const { query } = useArtistListFilters();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const ArtistListInfiniteGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: ArtistListInfiniteGridProps) => {
|
}: ArtistListInfiniteGridProps) => {
|
||||||
const listCountQuery = artistsQueries.artistListCount({
|
const listCountQuery = artistsQueries.artistListCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -64,6 +65,7 @@ export const ArtistListInfiniteGrid = ({
|
|||||||
onRangeChanged={onRangeChanged}
|
onRangeChanged={onRangeChanged}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const ArtistListPaginatedGrid = ({
|
|||||||
},
|
},
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
|
size,
|
||||||
}: ArtistListPaginatedGridProps) => {
|
}: ArtistListPaginatedGridProps) => {
|
||||||
const listCountQuery = artistsQueries.artistListCount({
|
const listCountQuery = artistsQueries.artistListCount({
|
||||||
query: { ...query },
|
query: { ...query },
|
||||||
@@ -76,6 +77,7 @@ export const ArtistListPaginatedGrid = ({
|
|||||||
itemType={LibraryItem.ARTIST}
|
itemType={LibraryItem.ARTIST}
|
||||||
onScrollEnd={handleOnScrollEnd}
|
onScrollEnd={handleOnScrollEnd}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
size={size}
|
||||||
/>
|
/>
|
||||||
</ItemListWithPagination>
|
</ItemListWithPagination>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { useRef } from 'react';
|
import { Suspense, useRef } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
|
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
|
||||||
@@ -16,9 +17,10 @@ import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library
|
|||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { useFastAverageColor, useWaitForColorCalculation } from '/@/renderer/hooks';
|
import { useFastAverageColor, useWaitForColorCalculation } from '/@/renderer/hooks';
|
||||||
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const AlbumArtistDetailRoute = () => {
|
const AlbumArtistDetailRouteContent = () => {
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const headerRef = useRef<HTMLDivElement>(null);
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
@@ -31,18 +33,29 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
|
|
||||||
const routeId = (artistId || albumArtistId) as string;
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
|
|
||||||
const location = useLocation();
|
const detailQuery = useSuspenseQuery(
|
||||||
|
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
||||||
|
);
|
||||||
|
|
||||||
const detailQuery = useSuspenseQuery({
|
const imageUrl = useItemImageUrl({
|
||||||
...artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
id: detailQuery.data?.imageId || undefined,
|
||||||
initialData: location.state?.item,
|
imageUrl: detailQuery.data?.imageUrl,
|
||||||
staleTime: 0,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
type: 'header',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const libraryBackgroundImageUrl = useItemImageUrl({
|
||||||
|
id: detailQuery.data?.imageId || undefined,
|
||||||
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
type: 'itemCard',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedImageUrl = imageUrl || detailQuery.data?.imageUrl;
|
||||||
|
|
||||||
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
|
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
|
||||||
id: artistId,
|
id: artistId,
|
||||||
src: detailQuery.data?.imageUrl,
|
src: selectedImageUrl,
|
||||||
srcLoaded: !detailQuery.isLoading,
|
srcLoaded: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const background = backgroundColor;
|
const background = backgroundColor;
|
||||||
@@ -50,14 +63,14 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
const showBlurredImage = artistBackground;
|
const showBlurredImage = artistBackground;
|
||||||
|
|
||||||
const { isReady } = useWaitForColorCalculation({
|
const { isReady } = useWaitForColorCalculation({
|
||||||
hasImage: !!detailQuery.data?.imageUrl,
|
hasImage: !!selectedImageUrl,
|
||||||
isLoading: isColorLoading,
|
isLoading: isColorLoading,
|
||||||
routeId,
|
routeId,
|
||||||
showBlurredImage,
|
showBlurredImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
return null;
|
return <Spinner container />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +86,7 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
variant="default"
|
variant="default"
|
||||||
/>
|
/>
|
||||||
<LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
{detailQuery?.data?.name}
|
{detailQuery.data?.name}
|
||||||
</LibraryHeaderBar.Title>
|
</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
@@ -86,7 +99,7 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
<LibraryBackgroundImage
|
<LibraryBackgroundImage
|
||||||
blur={artistBackgroundBlur}
|
blur={artistBackgroundBlur}
|
||||||
headerRef={headerRef}
|
headerRef={headerRef}
|
||||||
imageUrl={detailQuery.data?.imageUrl || ''}
|
imageUrl={libraryBackgroundImageUrl || ''}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
||||||
@@ -100,6 +113,20 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AlbumArtistDetailRoute = () => {
|
||||||
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Spinner container />} key={`album-artist-detail-suspense-${routeId}`}>
|
||||||
|
<AlbumArtistDetailRouteContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const AlbumArtistDetailRouteWithBoundary = () => {
|
const AlbumArtistDetailRouteWithBoundary = () => {
|
||||||
return (
|
return (
|
||||||
<PageErrorBoundary>
|
<PageErrorBoundary>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||||
enableSelection
|
enableSelection
|
||||||
|
enableSelectionDialog={false}
|
||||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||||
itemType={LibraryItem.SONG}
|
itemType={LibraryItem.SONG}
|
||||||
onColumnReordered={handleColumnReordered}
|
onColumnReordered={handleColumnReordered}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { useCurrentServerId, useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
|
import { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||||
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface PlayArtistRadioActionProps {
|
||||||
|
artist: AlbumArtist | Artist;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayArtistRadioAction = ({ artist, disabled }: PlayArtistRadioActionProps) => {
|
||||||
|
const { artistRadioCount } = useGeneralSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const player = usePlayer();
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
|
const handlePlayArtistRadio = useCallback(
|
||||||
|
async (playType: Play) => {
|
||||||
|
if (!serverId || !artist) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const artistRadioSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.artistRadio({
|
||||||
|
query: {
|
||||||
|
artistId: artist.id,
|
||||||
|
count: artistRadioCount,
|
||||||
|
},
|
||||||
|
serverId: serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ artistId: artist.id }),
|
||||||
|
});
|
||||||
|
if (artistRadioSongs && artistRadioSongs.length > 0) {
|
||||||
|
player.addToQueueByData(artistRadioSongs, playType);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load track radio:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[artist, artistRadioCount, player, queryClient, serverId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePlayArtistRadioNow = useCallback(() => {
|
||||||
|
handlePlayArtistRadio(Play.NOW);
|
||||||
|
}, [handlePlayArtistRadio]);
|
||||||
|
|
||||||
|
const handlePlayArtistRadioNext = useCallback(() => {
|
||||||
|
handlePlayArtistRadio(Play.NEXT);
|
||||||
|
}, [handlePlayArtistRadio]);
|
||||||
|
|
||||||
|
const handlePlayArtistRadioLast = useCallback(() => {
|
||||||
|
handlePlayArtistRadio(Play.LAST);
|
||||||
|
}, [handlePlayArtistRadio]);
|
||||||
|
|
||||||
|
const defaultPlayArtistRadioAction = useCallback(() => {
|
||||||
|
handlePlayArtistRadio(playButtonBehavior);
|
||||||
|
}, [handlePlayArtistRadio, playButtonBehavior]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu.Submenu>
|
||||||
|
<ContextMenu.SubmenuTarget>
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={disabled}
|
||||||
|
leftIcon="radio"
|
||||||
|
onSelect={defaultPlayArtistRadioAction}
|
||||||
|
rightIcon="arrowRightS"
|
||||||
|
>
|
||||||
|
{t('player.artistRadio', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.SubmenuTarget>
|
||||||
|
<ContextMenu.SubmenuContent>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlay" onSelect={handlePlayArtistRadioNow}>
|
||||||
|
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlayNext" onSelect={handlePlayArtistRadioNext}>
|
||||||
|
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlayLast" onSelect={handlePlayArtistRadioLast}>
|
||||||
|
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.SubmenuContent>
|
||||||
|
</ContextMenu.Submenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { useCurrentServerId, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface PlayTrackRadioActionProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
song: Song;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayTrackRadioAction = ({ disabled, song }: PlayTrackRadioActionProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const player = usePlayer();
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
|
const handlePlayTrackRadio = useCallback(
|
||||||
|
async (playType: Play) => {
|
||||||
|
if (!serverId || !song) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const similarSongs = await queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
songId: song.id,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ similarSongs: song.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (similarSongs && similarSongs.length > 0) {
|
||||||
|
player.addToQueueByData(similarSongs, playType);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load track radio:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[player, queryClient, serverId, song],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePlayTrackRadioNow = useCallback(() => {
|
||||||
|
handlePlayTrackRadio(Play.NOW);
|
||||||
|
}, [handlePlayTrackRadio]);
|
||||||
|
|
||||||
|
const handlePlayTrackRadioNext = useCallback(() => {
|
||||||
|
handlePlayTrackRadio(Play.NEXT);
|
||||||
|
}, [handlePlayTrackRadio]);
|
||||||
|
|
||||||
|
const handlePlayTrackRadioLast = useCallback(() => {
|
||||||
|
handlePlayTrackRadio(Play.LAST);
|
||||||
|
}, [handlePlayTrackRadio]);
|
||||||
|
|
||||||
|
const defaultPlayTrackRadioAction = useCallback(() => {
|
||||||
|
handlePlayTrackRadio(playButtonBehavior);
|
||||||
|
}, [handlePlayTrackRadio, playButtonBehavior]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu.Submenu>
|
||||||
|
<ContextMenu.SubmenuTarget>
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={disabled}
|
||||||
|
leftIcon="radio"
|
||||||
|
onSelect={defaultPlayTrackRadioAction}
|
||||||
|
rightIcon="arrowRightS"
|
||||||
|
>
|
||||||
|
{t('player.trackRadio', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.SubmenuTarget>
|
||||||
|
<ContextMenu.SubmenuContent>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlay" onSelect={handlePlayTrackRadioNow}>
|
||||||
|
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlayNext" onSelect={handlePlayTrackRadioNext}>
|
||||||
|
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item leftIcon="mediaPlayLast" onSelect={handlePlayTrackRadioLast}>
|
||||||
|
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.SubmenuContent>
|
||||||
|
</ContextMenu.Submenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
|
import { useCurrentServer, useCurrentServerId, useGeneralSettings } from '/@/renderer/store';
|
||||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
@@ -17,6 +17,7 @@ export const SetRatingAction = ({ ids, itemType }: SetRatingActionProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
const { showRatings } = useGeneralSettings();
|
||||||
|
|
||||||
const setRatingMutation = useSetRating({});
|
const setRatingMutation = useSetRating({});
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ export const SetRatingAction = ({ ids, itemType }: SetRatingActionProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isRatingSupported) {
|
if (!showRatings || !isRatingSupported) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -1,8 +1,8 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import styles from './context-menu-preview.module.css';
|
import styles from './context-menu-preview.module.css';
|
||||||
|
|
||||||
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
@@ -26,6 +26,10 @@ const getItemName = (item: unknown): string => {
|
|||||||
|
|
||||||
const getItemImage = (item: unknown): null | string => {
|
const getItemImage = (item: unknown): null | string => {
|
||||||
if (item && typeof item === 'object') {
|
if (item && typeof item === 'object') {
|
||||||
|
if ('imageId' in item && typeof item.imageId === 'string') {
|
||||||
|
return item.imageId;
|
||||||
|
}
|
||||||
|
|
||||||
if ('imageUrl' in item && typeof item.imageUrl === 'string') {
|
if ('imageUrl' in item && typeof item.imageUrl === 'string') {
|
||||||
return item.imageUrl;
|
return item.imageUrl;
|
||||||
}
|
}
|
||||||
@@ -33,7 +37,7 @@ const getItemImage = (item: unknown): null | string => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewProps) => {
|
export const ContextMenuPreview = ({ items, itemType }: ContextMenuPreviewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const itemCount = items.length;
|
const itemCount = items.length;
|
||||||
const firstItem = items[0];
|
const firstItem = items[0];
|
||||||
@@ -41,6 +45,12 @@ export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewP
|
|||||||
const itemImage = firstItem ? getItemImage(firstItem) : null;
|
const itemImage = firstItem ? getItemImage(firstItem) : null;
|
||||||
const isMultiple = itemCount > 1;
|
const isMultiple = itemCount > 1;
|
||||||
|
|
||||||
|
const imageUrl = useItemImageUrl({
|
||||||
|
id: (firstItem as { imageId?: string })?.imageId,
|
||||||
|
itemType: itemType || LibraryItem.SONG,
|
||||||
|
type: 'table',
|
||||||
|
});
|
||||||
|
|
||||||
if (itemCount === 0) {
|
if (itemCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -52,7 +62,7 @@ export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewP
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{itemImage ? (
|
{itemImage ? (
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<img alt={itemName} className={styles.image} src={itemImage} />
|
<img alt={itemName} className={styles.image} src={imageUrl} />
|
||||||
<div className={styles.imageOverlay} />
|
<div className={styles.imageOverlay} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -85,6 +95,6 @@ export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewP
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
ContextMenuPreview.displayName = 'ContextMenuPreview';
|
ContextMenuPreview.displayName = 'ContextMenuPreview';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user