mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
140 Commits
| 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 | |||
| c9cd87bae5 | |||
| 9a8cb45510 | |||
| 68b6a58ac5 | |||
| 5b5cdbfb7f | |||
| cf4e505743 | |||
| 8464ed439e | |||
| 9e49a45db9 | |||
| 8dc5f2a580 | |||
| 6bb848a675 | |||
| 8edf61f9e7 | |||
| 96d2699a2d | |||
| 614761efd7 |
@@ -4,9 +4,24 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- development
|
||||
paths:
|
||||
- 'src/**'
|
||||
|
||||
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:
|
||||
needs: wait-for-lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
|
||||
@@ -105,18 +105,15 @@ services:
|
||||
feishin:
|
||||
container_name: feishin
|
||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||
restart: unless-stopped
|
||||
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_TYPE=jellyfin # navidrome also works
|
||||
- SERVER_URL= # http://address:port
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- UMASK=002
|
||||
- TZ=America/Los_Angeles
|
||||
- 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:
|
||||
- 9180:9180
|
||||
restart: unless-stopped
|
||||
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
version: '3.5'
|
||||
services:
|
||||
feishin:
|
||||
container_name: feishin
|
||||
image: ghcr.io/jeffvli/feishin:latest
|
||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||
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:
|
||||
- 9180:9180
|
||||
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 # navidrome also works
|
||||
- SERVER_URL= # http://address:port
|
||||
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||
Generated
+18279
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.22.0",
|
||||
"version": "1.0.1",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
@@ -82,6 +82,8 @@
|
||||
"@xhayper/discord-rpc": "^1.3.0",
|
||||
"audiomotion-analyzer": "^4.5.1",
|
||||
"axios": "^1.13.2",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"cheerio": "^1.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -121,6 +123,7 @@
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-player": "^2.16.0",
|
||||
"react-router": "^7.9.6",
|
||||
"react-split-pane": "^3.0.4",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"react-window-v2": "npm:react-window@^2.2.3",
|
||||
|
||||
Generated
+65
-3
@@ -71,6 +71,12 @@ importers:
|
||||
axios:
|
||||
specifier: ^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:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
@@ -188,6 +194,9 @@ importers:
|
||||
react-router:
|
||||
specifier: ^7.9.6
|
||||
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:
|
||||
specifier: ^1.0.26
|
||||
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -2263,6 +2272,9 @@ packages:
|
||||
peerDependencies:
|
||||
'@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:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -2359,6 +2371,12 @@ packages:
|
||||
builder-util@26.0.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2555,6 +2573,10 @@ packages:
|
||||
core-js-compat@3.47.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
|
||||
@@ -2779,6 +2801,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecma-proposal-math-extensions@0.0.2:
|
||||
resolution: {integrity: sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==}
|
||||
|
||||
ejs@3.1.10:
|
||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4621,6 +4646,13 @@ packages:
|
||||
react-dom:
|
||||
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:
|
||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4688,6 +4720,9 @@ packages:
|
||||
regenerate@1.4.2:
|
||||
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
||||
|
||||
regenerator-runtime@0.11.1:
|
||||
resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7998,6 +8033,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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@2.0.0: {}
|
||||
@@ -8134,6 +8174,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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: {}
|
||||
|
||||
cacache@16.1.3:
|
||||
@@ -8361,6 +8412,8 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.28.0
|
||||
|
||||
core-js@2.6.12: {}
|
||||
|
||||
core-util-is@1.0.2:
|
||||
optional: true
|
||||
|
||||
@@ -8604,6 +8657,8 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecma-proposal-math-extensions@0.0.2: {}
|
||||
|
||||
ejs@3.1.10:
|
||||
dependencies:
|
||||
jake: 10.9.2
|
||||
@@ -9526,7 +9581,7 @@ snapshots:
|
||||
|
||||
i18next@24.2.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
@@ -10588,6 +10643,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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):
|
||||
dependencies:
|
||||
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):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
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)
|
||||
@@ -10607,7 +10667,7 @@ snapshots:
|
||||
|
||||
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
@@ -10672,6 +10732,8 @@ snapshots:
|
||||
|
||||
regenerate@1.4.2: {}
|
||||
|
||||
regenerator-runtime@0.11.1: {}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
||||
+52
-13
@@ -14,7 +14,8 @@
|
||||
"tracks": "$t(entity.track_other)",
|
||||
"nowPlaying": "ara sona",
|
||||
"shared": "$t(entity.playlist_other) compartida",
|
||||
"favorites": "$t(entity.favorite_other)"
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"relatedArtists": "$t(entity.artist_other) similars",
|
||||
@@ -184,6 +185,9 @@
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "emissores de ràdio"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -301,7 +305,8 @@
|
||||
"sort": "ordre",
|
||||
"gridRows": "files de la quadrícula",
|
||||
"tableColumns": "columnes de la taula",
|
||||
"itemsMore": "{{count}} més"
|
||||
"itemsMore": "{{count}} més",
|
||||
"countSelected": "{{count}} seleccionats"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "àlbum",
|
||||
@@ -355,7 +360,13 @@
|
||||
"song_other": "cançons",
|
||||
"favorite_one": "preferit",
|
||||
"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": {
|
||||
"addToPlaylist": {
|
||||
@@ -445,6 +456,16 @@
|
||||
"input_played_optionAll": "totes les pistes",
|
||||
"input_played_optionUnplayed": "només les pistes sense reproduir",
|
||||
"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": {
|
||||
@@ -479,7 +500,13 @@
|
||||
"shuffle": "mescla",
|
||||
"shuffleAll": "mescla-ho tot",
|
||||
"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": {
|
||||
"language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))",
|
||||
@@ -643,8 +670,6 @@
|
||||
"playbackStyle_optionNormal": "normal",
|
||||
"playButtonBehavior": "comportament del botó de reproducció",
|
||||
"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_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa",
|
||||
"remotePassword": "contrasenya del servidor de control remot",
|
||||
@@ -787,7 +812,9 @@
|
||||
"queryBuilderCustomFields_inputLabel": "discogràfica",
|
||||
"queryBuilderCustomFields_inputTag": "etiqueta",
|
||||
"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": {
|
||||
"column": {
|
||||
@@ -953,8 +980,8 @@
|
||||
"repeat_all": "repetició",
|
||||
"shuffle": "reprodueix (mesclat)",
|
||||
"shuffle_off": "reproducció aleatòria desactivada",
|
||||
"addLast": "afegeix al final",
|
||||
"addNext": "afegeix a continuació",
|
||||
"addLast": "al final",
|
||||
"addNext": "a continuació",
|
||||
"favorite": "marcar com a preferida",
|
||||
"mute": "silencia",
|
||||
"next": "següent",
|
||||
@@ -970,12 +997,15 @@
|
||||
"toggleFullscreenPlayer": "activa el reproductor de pantalla completa",
|
||||
"unfavorite": "elimina de preferits",
|
||||
"pause": "pausa",
|
||||
"addLastShuffled": "afegeix al final (mesclat)",
|
||||
"addNextShuffled": "afegeix a continuació (mesclat)",
|
||||
"addLastShuffled": "al final (mesclat)",
|
||||
"addNextShuffled": "a continuació (mesclat)",
|
||||
"holdToShuffle": "mantén premut per mesclar",
|
||||
"queueType": "tipus de cua",
|
||||
"queueType_default": "predeterminat",
|
||||
"queueType_priority": "prioritat"
|
||||
"queueType_priority": "prioritat",
|
||||
"lyrics": "lletra",
|
||||
"restoreQueueFromServer": "restaura la cua del servidor",
|
||||
"saveQueueToServer": "desa la cua al servidor"
|
||||
},
|
||||
"error": {
|
||||
"credentialsRequired": "credencials requerides",
|
||||
@@ -1001,7 +1031,10 @@
|
||||
"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à",
|
||||
"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": {
|
||||
"primary": {
|
||||
@@ -1055,5 +1088,11 @@
|
||||
"queryBuilder": {
|
||||
"standardTags": "etiquetes estàndard",
|
||||
"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í",
|
||||
"lyrics": "texty",
|
||||
"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": {
|
||||
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
||||
@@ -207,8 +209,6 @@
|
||||
"passwordStore": "ukládání hesel / tajných klíčů",
|
||||
"mpvExtraParameters_help": "jeden na řádek",
|
||||
"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",
|
||||
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
||||
"externalLinks": "zobrazit externí odkazy",
|
||||
@@ -349,7 +349,18 @@
|
||||
"logLevel_optionInfo": "informace",
|
||||
"logLevel_optionWarn": "varování",
|
||||
"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": {
|
||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||
@@ -385,7 +396,11 @@
|
||||
"holdToMoveToTop": "podržte pro přesunutí nahoru",
|
||||
"holdToMoveToBottom": "podržte pro přesunutí dolů",
|
||||
"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": {
|
||||
"backward": "zpátky",
|
||||
@@ -502,7 +517,9 @@
|
||||
"tableColumns": "sloupce tabulky",
|
||||
"itemsMore": "{{count}} dalších",
|
||||
"noFilters": "nejsou nastaveny žádné filtry",
|
||||
"view": "zobrazit"
|
||||
"view": "zobrazit",
|
||||
"countSelected": "vybráno {{count}}",
|
||||
"retry": "zkusit znovu"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -634,7 +651,10 @@
|
||||
"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",
|
||||
"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": {
|
||||
"mostPlayed": "nejvíce přehráváno",
|
||||
@@ -804,7 +824,8 @@
|
||||
"transcoding": "překódování",
|
||||
"discord": "discord",
|
||||
"playerFilters": "filtry přehrávače",
|
||||
"logger": "protokol"
|
||||
"logger": "protokol",
|
||||
"lyricsDisplay": "zobrazení textů"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -970,6 +991,11 @@
|
||||
"input_homepageUrl": "adresa domovské stránky",
|
||||
"input_name": "název",
|
||||
"input_streamUrl": "adresa streamu"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "exportovat texty",
|
||||
"input_synced": "exportovat synchronizované texty",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -1084,5 +1110,157 @@
|
||||
"notInPlaylist": "není v",
|
||||
"notInTheLast": "není v posledním",
|
||||
"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": {
|
||||
"editPlaylist": "$t(entity.playlist_one) bearbeiten",
|
||||
"clearQueue": "Warteschlange leeren",
|
||||
"clearQueue": "Wiedergabeliste leeren",
|
||||
"addToFavorites": "Zu $t(entity.favorite_other) hinzufügen",
|
||||
"addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen",
|
||||
"createPlaylist": "$t(entity.playlist_one) erstellen",
|
||||
@@ -13,7 +13,7 @@
|
||||
"removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen",
|
||||
"viewPlaylists": "$t(entity.playlist_other) anzeigen",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromQueue": "Aus Warteschlange entfernen",
|
||||
"removeFromQueue": "Aus Wiedergabeliste entfernen",
|
||||
"setRating": "Bewerten",
|
||||
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
||||
"removeFromFavorites": "Aus $t(entity.favorite_other) entfernen",
|
||||
@@ -29,7 +29,11 @@
|
||||
"shuffleSelected": "Ausgewählte zufällig wiedergeben",
|
||||
"viewMore": "Mehr zeigen",
|
||||
"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": {
|
||||
"backward": "zurück",
|
||||
@@ -118,11 +122,11 @@
|
||||
"close": "schließen",
|
||||
"share": "Teilen",
|
||||
"translation": "Übersetzung",
|
||||
"trackGain": "Track-Pegelverstärkung",
|
||||
"trackPeak": "Track-Spitzenpegel",
|
||||
"trackGain": "Track Gain",
|
||||
"trackPeak": "Track Peak",
|
||||
"codec": "Codec",
|
||||
"albumPeak": "Album-Spitzenpegel",
|
||||
"albumGain": "Album-Pegelverstärkung",
|
||||
"albumGain": "Album Gain",
|
||||
"tags": "tags",
|
||||
"viewReleaseNotes": "Veröffentlichungsnotizen anzeigen",
|
||||
"newVersion": "eine neue Version wurde installiert ({{version}})",
|
||||
@@ -145,7 +149,8 @@
|
||||
"recordLabel": "Plattenlabel",
|
||||
"slower": "langsamer",
|
||||
"releaseType": "Veröffentlichungsformat",
|
||||
"view": "Betrachten"
|
||||
"view": "Betrachten",
|
||||
"countSelected": "{{count}} ausgewählt"
|
||||
},
|
||||
"error": {
|
||||
"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",
|
||||
"invalidServer": "Ungültiger Server",
|
||||
"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",
|
||||
"openError": "datei kann nicht geöffnet werden",
|
||||
"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": {
|
||||
"mostPlayed": "Meistgespielt",
|
||||
@@ -284,7 +291,7 @@
|
||||
"setExpiration": "Ablaufdatum setzen",
|
||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
||||
"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?)"
|
||||
},
|
||||
"privateMode": {
|
||||
@@ -293,7 +300,7 @@
|
||||
"title": "Privatmodus"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"shuffleAll": {
|
||||
@@ -303,9 +310,19 @@
|
||||
"input_minYear": "ab Jahr",
|
||||
"input_maxYear": "bis Jahr",
|
||||
"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": "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": {
|
||||
@@ -343,7 +360,11 @@
|
||||
"play_one": "{{count}} Wiedergabe",
|
||||
"play_other": "{{count}} Wiedergaben",
|
||||
"song_one": "Lied",
|
||||
"song_other": "Lieder"
|
||||
"song_other": "Lieder",
|
||||
"radioStation_one": "Radiosender",
|
||||
"radioStation_other": "Radiosender",
|
||||
"radioStationWithCount_one": "{{count}} Radiosender",
|
||||
"radioStationWithCount_other": "{{count}} Radiosender"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -359,7 +380,17 @@
|
||||
"displayType": "Anzeigestil",
|
||||
"autoFitColumns": "automatisch Spalten einpassen",
|
||||
"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": {
|
||||
"dateAdded": "Hinzugefügt am",
|
||||
@@ -387,7 +418,14 @@
|
||||
"title": "$t(common.title)",
|
||||
"year": "$t(common.year)",
|
||||
"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": {
|
||||
@@ -413,7 +451,11 @@
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"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": {
|
||||
@@ -522,7 +564,8 @@
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "$t(entity.playlist_other) geteilt",
|
||||
"myLibrary": "meine bibliothek",
|
||||
"favorites": "$t(entity.favorite_other)"
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "Wiedergabe",
|
||||
@@ -538,7 +581,7 @@
|
||||
"application": "App",
|
||||
"queryBuilder": "Abfrage-Editor",
|
||||
"theme": "Erscheinungsbild",
|
||||
"controls": "Steuerung",
|
||||
"controls": "Steuerelemente",
|
||||
"sidebar": "Seitenleiste",
|
||||
"scrobble": "Scrobbeln",
|
||||
"audio": "Audio",
|
||||
@@ -601,14 +644,17 @@
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "Neuanordnung nur bei Sortierung nach ID möglich"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "Radiosender"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"next": "nächster",
|
||||
"addNext": "Als Nächstes spielen",
|
||||
"addNext": "als Nächstes",
|
||||
"play": "Abspielen",
|
||||
"muted": "stummgeschaltet",
|
||||
"addLast": "Als Letztes spielen",
|
||||
"addLast": "als Letztes",
|
||||
"mute": "Stumm",
|
||||
"playRandom": "Zufällige Wiedergabe",
|
||||
"previous": "Vorheriger",
|
||||
@@ -617,7 +663,7 @@
|
||||
"playbackFetchInProgress": "lieder werden geladen…",
|
||||
"playbackSpeed": "Wiedergabegeschwindigkeit",
|
||||
"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": "Wiederholen",
|
||||
"queue_remove": "Ausgewählte entfernen",
|
||||
@@ -634,13 +680,15 @@
|
||||
"skip_forward": "vorspulen",
|
||||
"skip": "Überspringen",
|
||||
"playSimilarSongs": "Ähnliche Lieder abspielen",
|
||||
"viewQueue": "Warteschlange anzeigen",
|
||||
"addLastShuffled": "Als Letztes spielen (zufällige Wiedergabe)",
|
||||
"addNextShuffled": "Als Nächstes spielen (zufällige Wiedergabe)",
|
||||
"viewQueue": "Wiedergabeliste anzeigen",
|
||||
"addLastShuffled": "als Letztes (zufällige Wiedergabe)",
|
||||
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
|
||||
"queueType_default": "Standard",
|
||||
"queueType_priority": "Priorität",
|
||||
"holdToShuffle": "Halten für Zufallswiedergabe",
|
||||
"queueType": "Warteschlangentyp"
|
||||
"queueType": "Wiedergabelistentyp",
|
||||
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
||||
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
|
||||
},
|
||||
"setting": {
|
||||
"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",
|
||||
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
|
||||
"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",
|
||||
"hotkey_rate5": "Bewertung 5 Sterne",
|
||||
"hotkey_playbackPrevious": "Vorheriger Track",
|
||||
@@ -727,18 +775,18 @@
|
||||
"playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll",
|
||||
"mpvExecutablePath": "Pfad der ausführbaren MPV-Datei",
|
||||
"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",
|
||||
"hotkey_rate4": "Bewertung 4 Sterne",
|
||||
"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",
|
||||
"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",
|
||||
"playButtonBehavior": "Verhalten der Wiedergabetaste",
|
||||
"volumeWheelStep": "Lautstärkeänderung mit Mausrad",
|
||||
"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",
|
||||
"playbackStyle_optionNormal": "Normal",
|
||||
"windowBarStyle": "Fensterleistenstil",
|
||||
@@ -769,7 +817,7 @@
|
||||
"gaplessAudio_optionWeak": "schwach (empfohlen)",
|
||||
"minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)",
|
||||
"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",
|
||||
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
||||
"fontType_optionSystem": "System Schriftart",
|
||||
@@ -795,33 +843,33 @@
|
||||
"clearCache": "Browser-Zwischenspeicher löschen",
|
||||
"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",
|
||||
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
|
||||
"sidePlayQueueStyle": "Stil der Wiedergabeliste in der Seitenleiste",
|
||||
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
||||
"zoom": "Zoom",
|
||||
"albumBackground": "Album Hintergrund",
|
||||
"customCss": "Benutzerdefiniert css",
|
||||
"customCss": "Benutzerdefiniertes CSS",
|
||||
"homeConfiguration": "Startseite Konfiguration",
|
||||
"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_description": "Status als hört zu statt als spielt anzeigen",
|
||||
"lastfm": "zeige last.fm links",
|
||||
"lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten",
|
||||
"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",
|
||||
"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",
|
||||
"clearCacheSuccess": "Cache erfolgreich geleert",
|
||||
"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_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
||||
"artistConfiguration": "künstler Albumseite Konfiguration",
|
||||
"buttonSize": "spielerleisten-Knopfgröße",
|
||||
"buttonSize_description": "die Größe der Spieler-Knöpfe",
|
||||
"hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus",
|
||||
"replayGainFallback": "{{ReplayGain}} Rückgriff",
|
||||
"replayGainFallback": "{{ReplayGain}} Alternative",
|
||||
"replayGainClipping": "{{ReplayGain}} Clipping",
|
||||
"exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren",
|
||||
"exportImportSettings_control_exportText": "Einstellungen exportieren",
|
||||
@@ -833,9 +881,7 @@
|
||||
"exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}",
|
||||
"language": "Sprache",
|
||||
"playerAlbumArtResolution": "Auflösung des Albumcovers",
|
||||
"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_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
@@ -844,11 +890,11 @@
|
||||
"logLevel_optionError": "Fehler",
|
||||
"logLevel_optionInfo": "Info",
|
||||
"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_itemCount": "Anzahl",
|
||||
"autoDJ_itemCount_description": "Die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Warteschlange 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_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 Wiedergabeliste befinden, bevor Auto DJ ausgelöst wird",
|
||||
"autoDJ_timing": "Timing",
|
||||
"discordDisplayType": "{{discord}} Presence Darstellungsart",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz",
|
||||
@@ -867,11 +913,11 @@
|
||||
"neteaseTranslation": "NetEase Übersetzungen aktivieren",
|
||||
"notify": "Benachrichtigungen aktivieren",
|
||||
"notify_description": "Zeigt Benachrichtigungen beim Titelwechsel",
|
||||
"playerFilters": "Lieder in der Warteschlange filtern",
|
||||
"playerFilters": "Lieder der Wiedergabeliste filtern",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"volumeWidth_description": "Die Breite des Lautstärkereglers",
|
||||
"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",
|
||||
"trayEnabled": "Info-Symbol anzeigen",
|
||||
"transcode": "Transkodierung aktivieren",
|
||||
@@ -889,7 +935,7 @@
|
||||
"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",
|
||||
"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",
|
||||
"releaseChannel_optionBeta": "Beta",
|
||||
"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!",
|
||||
"followCurrentSong": "aktuellem Titel folgen",
|
||||
"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": "Priorisiere lokale Songtexte",
|
||||
"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": {
|
||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||
@@ -961,5 +1021,11 @@
|
||||
"soundtrack": "Soundtrack",
|
||||
"spokenWord": "Gesprochenes Wort"
|
||||
}
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "Min",
|
||||
"secondShort": "Sek",
|
||||
"hourShort": "Std",
|
||||
"dayShort": "Tag"
|
||||
}
|
||||
}
|
||||
|
||||
+183
-2
@@ -2,11 +2,14 @@
|
||||
"action": {
|
||||
"addToFavorites": "add to $t(entity.favorite_other)",
|
||||
"addToPlaylist": "add to $t(entity.playlist_one)",
|
||||
"addOrRemoveFromSelection": "add or remove from selection",
|
||||
"selectRangeOfItems": "select a range of items",
|
||||
"clearQueue": "clear queue",
|
||||
"createPlaylist": "create $t(entity.playlist_one)",
|
||||
"createRadioStation": "create $t(entity.radioStation_one)",
|
||||
"deletePlaylist": "delete $t(entity.playlist_one)",
|
||||
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
||||
"selectAll": "select all",
|
||||
"deselectAll": "deselect all",
|
||||
"downloadStarted": "started download of {{count}} items",
|
||||
"editPlaylist": "edit $t(entity.playlist_one)",
|
||||
@@ -30,12 +33,14 @@
|
||||
"toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor",
|
||||
"viewPlaylists": "view $t(entity.playlist_other)",
|
||||
"viewMore": "view more",
|
||||
"openApplicationDirectory": "open application directory",
|
||||
"openIn": {
|
||||
"lastfm": "Open in Last.fm",
|
||||
"musicbrainz": "Open in MusicBrainz"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"countSelected": "{{count}} selected",
|
||||
"explicitStatus": "explicit status",
|
||||
"action_one": "action",
|
||||
"action_other": "actions",
|
||||
@@ -114,6 +119,7 @@
|
||||
"quit": "quit",
|
||||
"random": "random",
|
||||
"rating": "rating",
|
||||
"retry": "retry",
|
||||
"recordLabel": "record label",
|
||||
"releaseType": "release type",
|
||||
"refresh": "refresh",
|
||||
@@ -207,6 +213,8 @@
|
||||
"mpvRequired": "MPV required",
|
||||
"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",
|
||||
"noNetwork": "server unavailable",
|
||||
"noNetworkDescription": "couldn't connect to this server",
|
||||
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
||||
"openError": "could not open file",
|
||||
"playbackError": "an error occurred when trying to play the media",
|
||||
@@ -266,6 +274,12 @@
|
||||
"trackNumber": "track",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "m",
|
||||
"secondShort": "s",
|
||||
"hourShort": "h",
|
||||
"dayShort": "d"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "is after",
|
||||
"afterDate": "is after (date)",
|
||||
@@ -341,6 +355,11 @@
|
||||
"success": "$t(entity.playlist_one) updated successfully",
|
||||
"title": "edit $t(entity.playlist_one)"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "export lyrics",
|
||||
"input_synced": "export synced lyrics",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist_one)",
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -391,6 +410,8 @@
|
||||
"albumArtistDetail": {
|
||||
"about": "About {{artist}}",
|
||||
"appearsOn": "appears on",
|
||||
"groupingTypeAll": "all release types",
|
||||
"groupingTypePrimary": "primary release types",
|
||||
"recentReleases": "recent releases",
|
||||
"viewDiscography": "view discography",
|
||||
"relatedArtists": "related $t(entity.artist_other)",
|
||||
@@ -550,6 +571,7 @@
|
||||
"scrobble": "scrobble",
|
||||
"audio": "audio",
|
||||
"lyrics": "lyrics",
|
||||
"lyricsDisplay": "lyrics display",
|
||||
"transcoding": "transcoding",
|
||||
"discord": "discord",
|
||||
"logger": "logger",
|
||||
@@ -583,6 +605,7 @@
|
||||
"addNext": "next",
|
||||
"addLastShuffled": "last (shuffled)",
|
||||
"addNextShuffled": "next (shuffled)",
|
||||
"artistRadio": "artist radio",
|
||||
"holdToShuffle": "hold to shuffle",
|
||||
"favorite": "favorite",
|
||||
"lyrics": "lyrics",
|
||||
@@ -618,6 +641,7 @@
|
||||
"skip_forward": "skip forwards",
|
||||
"stop": "stop",
|
||||
"toggleFullscreenPlayer": "toggle fullscreen player",
|
||||
"trackRadio": "track radio",
|
||||
"unfavorite": "unfavorite",
|
||||
"pause": "pause",
|
||||
"viewQueue": "view queue"
|
||||
@@ -849,8 +873,15 @@
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"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",
|
||||
"playerAlbumArtResolution": "player album art resolution",
|
||||
"artistRadioCount_description": "sets the number of songs to fetch for artist radio and track radio",
|
||||
"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": "playerbar fullscreen toggle",
|
||||
"playerbarSlider": "playerbar slider",
|
||||
@@ -868,8 +899,12 @@
|
||||
"preferLocalLyrics": "prefer local lyrics",
|
||||
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
|
||||
"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": "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": "preserve pitch",
|
||||
"audioFadeOnStatusChange": "audio fade on status change",
|
||||
@@ -1067,5 +1102,151 @@
|
||||
"error_oneFileOnly": "Please only select 1 file",
|
||||
"error_readingFile": "there has been an issue reading the file: {{errorMessage}}",
|
||||
"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",
|
||||
"lyrics": "Letras",
|
||||
"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": {
|
||||
"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": "iniciar minimizado",
|
||||
"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",
|
||||
"mpvExtraParameters_help": "Uno por línea",
|
||||
"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_optionWarn": "Advertencia",
|
||||
"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": {
|
||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||
@@ -385,7 +389,11 @@
|
||||
"moveUp": "Desplazar hacia arriba",
|
||||
"moveDown": "Desplazar hacia abajo",
|
||||
"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": {
|
||||
"backward": "hacia atrás",
|
||||
@@ -502,7 +510,9 @@
|
||||
"tableColumns": "Columnas de la tabla",
|
||||
"itemsMore": "{{count}} más",
|
||||
"noFilters": "Ningún filtro configurado",
|
||||
"view": "Vista"
|
||||
"view": "Vista",
|
||||
"countSelected": "{{count}} seleccionados",
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
||||
@@ -530,7 +540,10 @@
|
||||
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe",
|
||||
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto",
|
||||
"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": {
|
||||
"mostPlayed": "más reproducido",
|
||||
@@ -700,7 +713,8 @@
|
||||
"discord": "Discord",
|
||||
"sidebar": "Barra lateral",
|
||||
"playerFilters": "Filtros del reproductor",
|
||||
"logger": "Registrador"
|
||||
"logger": "Registrador",
|
||||
"lyricsDisplay": "Mostrar letras"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -866,6 +880,11 @@
|
||||
"input_homepageUrl": "URL de la página de inicio",
|
||||
"input_name": "Nombre",
|
||||
"input_streamUrl": "URL de la transmisión"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "Exportar letras",
|
||||
"input_synced": "Exportar letras sincronizadas",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1084,5 +1103,11 @@
|
||||
"notInTheLast": "no está en el último",
|
||||
"startsWith": "empieza con",
|
||||
"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",
|
||||
"playButtonBehavior": "erreprodukzio botoiaren portaera",
|
||||
"playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean",
|
||||
"playerAlbumArtResolution": "erreproduzitzailearen albumaren arte-azalaren erresoluzioa",
|
||||
"gaplessAudio": "hutsune gabeko audioa",
|
||||
"gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako",
|
||||
"passwordStore": "pasahitzak/biltegi sekretua",
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"replayGainClipping": "{{ReplayGain}} leikkaus",
|
||||
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
|
||||
"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)",
|
||||
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
|
||||
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
|
||||
@@ -490,7 +489,6 @@
|
||||
"sidebarConfiguration": "sivupalkin asetukset",
|
||||
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
|
||||
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
|
||||
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
|
||||
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
|
||||
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
|
||||
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
|
||||
|
||||
+10
-11
@@ -14,7 +14,7 @@
|
||||
"shuffle": "lecture (mélangé)",
|
||||
"playbackFetchNoResults": "aucun titre trouvé",
|
||||
"playbackFetchInProgress": "chargement des titres…",
|
||||
"addNext": "ajouter ensuite",
|
||||
"addNext": "prochain",
|
||||
"playbackSpeed": "vitesse de lecture",
|
||||
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
|
||||
"play": "lecture",
|
||||
@@ -24,15 +24,15 @@
|
||||
"queue_moveToTop": "déplacer la sélection vers le bas",
|
||||
"queue_moveToBottom": "déplacer la sélection vers le haut",
|
||||
"shuffle_off": "aléatoire désactivée",
|
||||
"addLast": "ajouter en dernier",
|
||||
"addLast": "dernier",
|
||||
"mute": "muet",
|
||||
"skip_forward": "avancer",
|
||||
"pause": "pause",
|
||||
"unfavorite": "retirer des favoris",
|
||||
"playSimilarSongs": "jouer des titres similaires",
|
||||
"viewQueue": "voir la file d'attente",
|
||||
"addLastShuffled": "ajouter en dernier (mélangé)",
|
||||
"addNextShuffled": "ajouter ensuite (mélangé)",
|
||||
"addLastShuffled": "dernier (mélangé)",
|
||||
"addNextShuffled": "prochain (mélangé)",
|
||||
"queueType": "type de file d'attente",
|
||||
"queueType_default": "défaut",
|
||||
"queueType_priority": "priorité",
|
||||
@@ -223,7 +223,8 @@
|
||||
"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",
|
||||
"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": {
|
||||
"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",
|
||||
"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",
|
||||
"playerAlbumArtResolution": "résolution de la pochette d'album du lecteur",
|
||||
"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",
|
||||
"startMinimized": "démarrer l'application en mode réduit",
|
||||
"transcode_description": "permet le transcodage vers différents formats",
|
||||
@@ -925,11 +924,11 @@
|
||||
"song_many": "titres",
|
||||
"song_other": "titres",
|
||||
"radioStation_one": "station radio",
|
||||
"radioStation_many": "stations radios",
|
||||
"radioStation_other": "",
|
||||
"radioStation_many": "stations radio",
|
||||
"radioStation_other": "stations radio",
|
||||
"radioStationWithCount_one": "{{count}} station radio",
|
||||
"radioStationWithCount_many": "{{count}} stations radios",
|
||||
"radioStationWithCount_other": ""
|
||||
"radioStationWithCount_many": "{{count}} stations radio",
|
||||
"radioStationWithCount_other": "{{count}} stations radio"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
|
||||
+50
-13
@@ -31,7 +31,13 @@
|
||||
"moveUp": "ugrás fel",
|
||||
"moveDown": "ugrás le",
|
||||
"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": {
|
||||
"collapse": "összecsukás",
|
||||
@@ -145,7 +151,9 @@
|
||||
"tableColumns": "táblázat oszlopok",
|
||||
"itemsMore": "{{count}} még több",
|
||||
"view": "nézet",
|
||||
"noFilters": "nincs konfigurált szűrő"
|
||||
"noFilters": "nincs konfigurált szűrő",
|
||||
"countSelected": "{{count}} kiválasztott",
|
||||
"retry": "újra"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_one": "Zenész",
|
||||
@@ -184,7 +192,9 @@
|
||||
"trackWithCount_one": "{{count}} sáv",
|
||||
"trackWithCount_other": "{{count}} sávok",
|
||||
"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": {
|
||||
"apiRouteError": "a kérést nem sikerült célba juttatni",
|
||||
@@ -210,7 +220,12 @@
|
||||
"serverRequired": "szerver szükséges",
|
||||
"serverNotSelectedError": "nincs szerver kiválasztva",
|
||||
"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": {
|
||||
"albumCount": "$t(entity.album_other) darab",
|
||||
@@ -345,6 +360,16 @@
|
||||
"input_played": "csak szűrt zenék",
|
||||
"input_played_optionUnplayed": "Csak a még nem lejátszottak",
|
||||
"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": {
|
||||
@@ -525,7 +550,8 @@
|
||||
"settings": "$t(common.setting_other)",
|
||||
"shared": "megosztott $t(entity.playlist_other)",
|
||||
"tracks": "$t(entity.track_other)",
|
||||
"favorites": "$t(entity.favorite_other)"
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "dalok tőle {{artist}}",
|
||||
@@ -537,11 +563,14 @@
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "rádió állomások"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "utoljára hozzáadva",
|
||||
"addNext": "következő hozzáadása",
|
||||
"addLast": "utolsónak",
|
||||
"addNext": "következő",
|
||||
"favorite": "kedvenc",
|
||||
"mute": "némítás",
|
||||
"muted": "némítva",
|
||||
@@ -571,13 +600,15 @@
|
||||
"pause": "szünet",
|
||||
"viewQueue": "műsorlista megtekintése",
|
||||
"shuffle_off": "kevert lejátszás ki",
|
||||
"addLastShuffled": "Hozzáadás a végére (keverve)",
|
||||
"addNextShuffled": "Hozzáadás következőnek (keverve)",
|
||||
"addLastShuffled": "végére (keverve)",
|
||||
"addNextShuffled": "következő (keverve)",
|
||||
"queueType": "lekérdezés típus",
|
||||
"queueType_default": "alapértelmezett",
|
||||
"queueType_priority": "prioritás",
|
||||
"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": {
|
||||
"primary": {
|
||||
@@ -766,7 +797,6 @@
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"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": "Minimális Scrobble arány (százalék)",
|
||||
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
|
||||
@@ -780,7 +810,6 @@
|
||||
"notify": "bekapcsolja a dal értesítéseket",
|
||||
"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",
|
||||
"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": "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",
|
||||
@@ -908,7 +937,9 @@
|
||||
"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",
|
||||
"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": {
|
||||
"config": {
|
||||
@@ -1038,5 +1069,11 @@
|
||||
"matchesRegex": "illeszkedik a regexre",
|
||||
"is": "van",
|
||||
"isNot": "nincs"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "perc",
|
||||
"secondShort": "mp",
|
||||
"hourShort": "óra",
|
||||
"dayShort": "nap"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,8 +579,6 @@
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"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_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
|
||||
"remotePassword": "kata sandi kontrol jarak jauh server",
|
||||
|
||||
@@ -355,8 +355,6 @@
|
||||
"passwordStore": "Archivio di password/segreti",
|
||||
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali",
|
||||
"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_optionDetached": "sganciata",
|
||||
"startMinimized": "avvia minimizzato",
|
||||
|
||||
@@ -203,7 +203,6 @@
|
||||
"volumeWidth_description": "音量スライダーの幅",
|
||||
"volumeWidth": "音量スライダーの幅",
|
||||
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
|
||||
"playerAlbumArtResolution_description": "大画面プレーヤーのアルバムアートプレビューの解像度。解像度が高いほど鮮明になりますが、読み込みが遅くなる可能性があります。デフォルトは 0 (自動設定) です",
|
||||
"mpvExtraParameters_help": "1 行に 1 つずつ",
|
||||
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
|
||||
"musicbrainz": "MusicBrainz リンクを表示する",
|
||||
@@ -211,7 +210,6 @@
|
||||
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
|
||||
"passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
|
||||
"passwordStore": "パスワード/シークレットストア",
|
||||
"playerAlbumArtResolution": "プレーヤーのアルバムアートの解像度",
|
||||
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
|
||||
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
|
||||
"preferLocalLyrics": "ローカル歌詞を優先する",
|
||||
|
||||
@@ -21,7 +21,23 @@
|
||||
},
|
||||
"viewPlaylists": "$t(entity.playlist_other) 보기",
|
||||
"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": {
|
||||
"translation": "번역",
|
||||
@@ -122,7 +138,18 @@
|
||||
"recordLabel": "레이블",
|
||||
"releaseType": "발매형태",
|
||||
"explicit": "성인컨텐츠",
|
||||
"clean": "클린"
|
||||
"clean": "클린",
|
||||
"countSelected": "{{count}}개 선택됨",
|
||||
"doNotShowAgain": "다시 보지 않기",
|
||||
"view": "보기",
|
||||
"externalLinks": "외부 링크",
|
||||
"faster": "빠르게",
|
||||
"noFilters": "필터 미설정",
|
||||
"slower": "천천히",
|
||||
"sort": "정렬",
|
||||
"gridRows": "행 그리드",
|
||||
"tableColumns": "테이블 열",
|
||||
"itemsMore": "{{count}}개 더"
|
||||
},
|
||||
"entity": {
|
||||
"albumWithCount_other": "{{count}} 앨범",
|
||||
@@ -142,7 +169,9 @@
|
||||
"play_other": "{{count}} 재생",
|
||||
"playlistWithCount_other": "{{count}} 재생목록",
|
||||
"smartPlaylist": "스마트 $t(entity.playlist_one)",
|
||||
"track_other": "트랙"
|
||||
"track_other": "트랙",
|
||||
"radioStation_other": "라디오 방송국",
|
||||
"radioStationWithCount_other": "{{count}}개 라디오 방송국"
|
||||
},
|
||||
"error": {
|
||||
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
|
||||
|
||||
@@ -27,7 +27,17 @@
|
||||
"shuffle": "shuffle",
|
||||
"shuffleAll": "shuffle alles",
|
||||
"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": {
|
||||
"backward": "achteruit",
|
||||
@@ -139,7 +149,10 @@
|
||||
"clean": "schoon",
|
||||
"gridRows": "rasterrijen",
|
||||
"tableColumns": "tabelkolommen",
|
||||
"itemsMore": "{{count}} meer"
|
||||
"itemsMore": "{{count}} meer",
|
||||
"countSelected": "{{count}} geselecteerd",
|
||||
"view": "bekijken",
|
||||
"noFilters": "geen filters ingesteld"
|
||||
},
|
||||
"filter": {
|
||||
"rating": "rating",
|
||||
@@ -427,7 +440,11 @@
|
||||
"song_one": "lied",
|
||||
"song_other": "liedjes",
|
||||
"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": {
|
||||
"column": {
|
||||
|
||||
+185
-9
@@ -33,7 +33,11 @@
|
||||
"holdToMoveToTop": "przytrzymaj aby, przesunąć na górę",
|
||||
"holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół",
|
||||
"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": {
|
||||
"increase": "zwiększ",
|
||||
@@ -150,7 +154,9 @@
|
||||
"tableColumns": "tabela kolumn",
|
||||
"itemsMore": "{{count}} więcej",
|
||||
"noFilters": "nie skonfigurowano filtrów",
|
||||
"view": "wyświetl"
|
||||
"view": "wyświetl",
|
||||
"countSelected": "wybrano {{count}}",
|
||||
"retry": "spróbuj ponownie"
|
||||
},
|
||||
"entity": {
|
||||
"genre_one": "gatunek",
|
||||
@@ -238,7 +244,10 @@
|
||||
"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",
|
||||
"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": {
|
||||
"mostPlayed": "najczęściej odtwarzane",
|
||||
@@ -383,6 +392,11 @@
|
||||
"input_homepageUrl": "url strony głównej",
|
||||
"input_name": "nazwa",
|
||||
"input_streamUrl": "url strumienia"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "eksportuj tekst",
|
||||
"input_synced": "eksportuj zsynchronizowany tekst",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -521,7 +535,8 @@
|
||||
"transcoding": "transkodowanie",
|
||||
"discord": "discord",
|
||||
"playerFilters": "filtry odtwarzacza",
|
||||
"logger": "logger"
|
||||
"logger": "logger",
|
||||
"lyricsDisplay": "wyświetlanie tekstu"
|
||||
},
|
||||
"trackList": {
|
||||
"title": "$t(entity.track_other)",
|
||||
@@ -548,7 +563,9 @@
|
||||
"viewDiscography": "przeglądaj dyskografię",
|
||||
"relatedArtists": "powiązane z $t(entity.artist_other)",
|
||||
"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": {
|
||||
"copyPath": "kopiuj ścieżkę do schowka",
|
||||
@@ -616,7 +633,9 @@
|
||||
"holdToShuffle": "przytrzymaj aby odtwarzać losowo",
|
||||
"lyrics": "tekst",
|
||||
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
||||
"saveQueueToServer": "zapisz kolejkę na serwerze"
|
||||
"saveQueueToServer": "zapisz kolejkę na serwerze",
|
||||
"artistRadio": "radio wykonawcy",
|
||||
"trackRadio": "radio utworu"
|
||||
},
|
||||
"setting": {
|
||||
"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",
|
||||
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
||||
"clearCache": "wyczyść pamięć podręczną przeglądarki",
|
||||
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
|
||||
"externalLinks": "pokaż zewnętrzne linki",
|
||||
"mpvExtraParameters_help": "po jednym na linię",
|
||||
"passwordStore": "hasła",
|
||||
"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_description": "uruchom aplikację w zasobniku systemowym",
|
||||
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
|
||||
@@ -926,7 +943,18 @@
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "ostrzeżenia",
|
||||
"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": {
|
||||
"config": {
|
||||
@@ -1084,5 +1112,153 @@
|
||||
"notInPlaylist": "nie jest w",
|
||||
"notInTheLast": "nie jest w ostatnim",
|
||||
"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_optionPlay": "$t(player.play)",
|
||||
"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_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia",
|
||||
"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",
|
||||
"scrobble": "Scrobblar",
|
||||
"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_description": "Exibir ou ocultar os botões de pular na barra do reprodutor",
|
||||
"showSkipButtons": "Exibir botões de pular",
|
||||
|
||||
@@ -670,7 +670,6 @@
|
||||
"playButtonBehavior": "поведение кнопки воспроизведения",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
|
||||
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
|
||||
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
|
||||
"remotePort": "порт сервера удалённого управления",
|
||||
@@ -711,7 +710,6 @@
|
||||
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
|
||||
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
|
||||
"playbackStyle": "стиль воспроизведения",
|
||||
"playerAlbumArtResolution": "разрешение обложки альбома",
|
||||
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
|
||||
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
|
||||
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
|
||||
|
||||
@@ -654,8 +654,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"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_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",
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
"musicbrainz": "Öppna i MusicBrainz"
|
||||
},
|
||||
"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": {
|
||||
"backward": "bakåt",
|
||||
@@ -143,7 +147,8 @@
|
||||
"clean": "städad",
|
||||
"gridRows": "rutnätsrader",
|
||||
"tableColumns": "tabellkolumner",
|
||||
"itemsMore": "{{count}} fler"
|
||||
"itemsMore": "{{count}} fler",
|
||||
"countSelected": "{{count}} markerade"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
||||
@@ -166,7 +171,12 @@
|
||||
"invalidServer": "ogiltig server",
|
||||
"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",
|
||||
"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": {
|
||||
"mostPlayed": "mest spelade",
|
||||
@@ -209,7 +219,9 @@
|
||||
"album": "$t(entity.album_one)",
|
||||
"trackNumber": "spår",
|
||||
"songCount": "sångräkning",
|
||||
"criticRating": "kritikerbetyg"
|
||||
"criticRating": "kritikerbetyg",
|
||||
"albumCount": "$t(entity.album_other) antal",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -236,13 +248,17 @@
|
||||
"input_savePassword": "spara lösenord",
|
||||
"ignoreSsl": "ignorera ssl ($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": {
|
||||
"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)",
|
||||
"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": {
|
||||
"title": "uppdatera server",
|
||||
@@ -258,7 +274,19 @@
|
||||
"title": "sångtext sök"
|
||||
},
|
||||
"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": {
|
||||
@@ -306,7 +334,17 @@
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
"play": "$t(player.play)",
|
||||
"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": {
|
||||
"moreFromArtist": "mer från $t(entity.artist_one)",
|
||||
@@ -340,6 +378,12 @@
|
||||
"searchFor": "sök efter {{query}}"
|
||||
},
|
||||
"title": "kommandon"
|
||||
},
|
||||
"manageServers": {
|
||||
"url": "URL",
|
||||
"username": "användarnamn",
|
||||
"editServerDetailsTooltip": "redigera serverinställningar",
|
||||
"removeServer": "ta bort server"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -405,5 +449,32 @@
|
||||
"queue_moveToBottom": "flytta markerad till toppen",
|
||||
"addLast": "lägg till sist",
|
||||
"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_optionAddLast": "$t(player.addLast)",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playerAlbumArtResolution": "பிளேயர் ஆல்பம் கலைத் தீர்மானம்",
|
||||
"playerAlbumArtResolution_description": "பெரிய வீரரின் ஆல்பம் கலை முன்னோட்டத்திற்கான தீர்மானம். பெரியது இது மிகவும் மிருதுவானதாக தோற்றமளிக்கிறது, ஆனால் மெதுவாக ஏற்றுவதை மெதுவாகக் கொண்டிருக்கலாம். இயல்புநிலை 0 க்கு, அதாவது ஆட்டோ",
|
||||
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
|
||||
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
|
||||
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
|
||||
|
||||
@@ -21,7 +21,15 @@
|
||||
"goToPage": "sayfaya git",
|
||||
"moveToNext": "sonrakine geç",
|
||||
"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": {
|
||||
"action_one": "eylem",
|
||||
@@ -120,7 +128,8 @@
|
||||
"trackGain": "parça kazancı",
|
||||
"trackPeak": "parça zirvesi",
|
||||
"private": "gizli",
|
||||
"clean": "temiz"
|
||||
"clean": "temiz",
|
||||
"countSelected": "{{count}} adet seçildi"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "albüm",
|
||||
@@ -604,8 +613,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"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_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir",
|
||||
"remotePassword": "uzaktan kontrol sunucusu şifresi",
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
"shuffle": "随机播放",
|
||||
"shuffleAll": "随机播放全部",
|
||||
"shuffleSelected": "随机播放选定的内容",
|
||||
"viewMore": "查看更多"
|
||||
"viewMore": "查看更多",
|
||||
"addOrRemoveFromSelection": "在所选内容中添加或移除",
|
||||
"selectRangeOfItems": "批量选择",
|
||||
"selectAll": "全选",
|
||||
"createRadioStation": "创建$t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "删除$t(entity.radioStation_one)"
|
||||
},
|
||||
"common": {
|
||||
"increase": "增高",
|
||||
@@ -189,7 +194,7 @@
|
||||
"queue_moveToTop": "将所选项移至底部",
|
||||
"queue_moveToBottom": "将所选项移至顶部",
|
||||
"shuffle_off": "禁用随机播放",
|
||||
"addLast": "添加至播放列表末尾",
|
||||
"addLast": "上一曲",
|
||||
"mute": "静音",
|
||||
"skip_forward": "向前跳过",
|
||||
"playbackSpeed": "播放速度",
|
||||
@@ -367,8 +372,6 @@
|
||||
"startMinimized_description": "在系统托盘中启动应用程序",
|
||||
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置",
|
||||
"clearCacheSuccess": "缓存清除成功",
|
||||
"playerAlbumArtResolution": "播放器专辑封面分辨率",
|
||||
"playerAlbumArtResolution_description": "大型播放器专辑封面预览的分辨率。较大使其看起来更清晰,但可能会减慢加载速度。默认为0,表示自动",
|
||||
"homeConfiguration": "主页配置",
|
||||
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
||||
"passwordStore": "密码/密钥存储",
|
||||
|
||||
@@ -106,7 +106,9 @@
|
||||
"explicitStatus": "Explicit狀態",
|
||||
"explicit": "Explicit",
|
||||
"gridRows": "網格行",
|
||||
"noFilters": "未設定任何過濾器"
|
||||
"noFilters": "未設定任何過濾器",
|
||||
"countSelected": "{{count}}個已選取",
|
||||
"retry": "重試"
|
||||
},
|
||||
"error": {
|
||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||
@@ -134,7 +136,10 @@
|
||||
"notificationDenied": "通知權限被拒絕。此設定無效",
|
||||
"openError": "無法開啟檔案",
|
||||
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
|
||||
"saveQueueFailed": "儲存播放佇列失敗"
|
||||
"saveQueueFailed": "儲存播放佇列失敗",
|
||||
"settingsSyncError": "偵測到渲染器與主程序之間的設定不一致,請重新啟動應用程式以套用變更",
|
||||
"noNetwork": "伺服器無法連線",
|
||||
"noNetworkDescription": "無法連接到此伺服器"
|
||||
},
|
||||
"page": {
|
||||
"contextMenu": {
|
||||
@@ -248,7 +253,8 @@
|
||||
"discord": "Discord",
|
||||
"queryBuilder": "查詢建構器",
|
||||
"playerFilters": "播放過濾器",
|
||||
"logger": "日誌記錄器"
|
||||
"logger": "日誌記錄器",
|
||||
"lyricsDisplay": "歌詞顯示"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -367,7 +373,9 @@
|
||||
"holdToShuffle": "按住以隨機",
|
||||
"lyrics": "歌詞",
|
||||
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
||||
"saveQueueToServer": "將播放佇列儲存至伺服器"
|
||||
"saveQueueToServer": "將播放佇列儲存至伺服器",
|
||||
"artistRadio": "藝人電台",
|
||||
"trackRadio": "曲目電台"
|
||||
},
|
||||
"setting": {
|
||||
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
||||
@@ -403,7 +411,7 @@
|
||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||
"enableRemote": "啟用遠端控制伺服器",
|
||||
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
||||
"exitToTray": "退出時最小化到系統匣",
|
||||
"exitToTray": "關閉時到將視窗最小化",
|
||||
"followLyric": "跟隨目前歌詞",
|
||||
"font_description": "設定應用程式使用的字體",
|
||||
"fontType": "字體類型",
|
||||
@@ -448,7 +456,7 @@
|
||||
"lyricOffset": "歌詞偏移(毫秒)",
|
||||
"lyricOffset_description": "將歌詞偏移指定的毫秒數",
|
||||
"lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序",
|
||||
"minimizeToTray": "最小化到匣",
|
||||
"minimizeToTray": "最小化到系統匣",
|
||||
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
||||
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
||||
@@ -572,11 +580,9 @@
|
||||
"passwordStore": "密碼/secret儲存",
|
||||
"passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "播放器專輯封面解析度",
|
||||
"playerAlbumArtResolution_description": "大型播放器專輯封面預覽的解析度。較大的解析度使其看起來更清晰,但可能會減慢載入速度。預設為 0,表示自動",
|
||||
"playerbarOpenDrawer": "播放器列全螢幕切換",
|
||||
"playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器",
|
||||
"startMinimized": "最小化啟動",
|
||||
"startMinimized": "啟動時最小化",
|
||||
"startMinimized_description": "在系統匣中啟動應用程式",
|
||||
"transcode_description": "啟用轉碼到不同格式",
|
||||
"transcodeBitrate": "要轉碼的比特率",
|
||||
@@ -677,7 +683,17 @@
|
||||
"logLevel_optionInfo": "Info",
|
||||
"logLevel_optionWarn": "Warn",
|
||||
"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": {
|
||||
"config": {
|
||||
@@ -817,7 +833,10 @@
|
||||
"holdToMoveToTop": "按住以移動至頂部",
|
||||
"holdToMoveToBottom": "按住以移動至底部",
|
||||
"createRadioStation": "創建 $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "刪除 $t(entity.radioStation_one)"
|
||||
"deleteRadioStation": "刪除 $t(entity.radioStation_one)",
|
||||
"openApplicationDirectory": "開啟應用程式目錄",
|
||||
"addOrRemoveFromSelection": "新增或移除選取項目",
|
||||
"selectAll": "全選"
|
||||
},
|
||||
"entity": {
|
||||
"album_other": "專輯",
|
||||
@@ -984,6 +1003,11 @@
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "已將播放佇列儲存至伺服器"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "匯出歌詞",
|
||||
"input_synced": "匯出同步歌詞",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"releaseType": {
|
||||
@@ -1035,5 +1059,14 @@
|
||||
"notContains": "不包含",
|
||||
"notInPlaylist": "不在…之中",
|
||||
"startsWith": "以…開頭"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "分",
|
||||
"secondShort": "秒",
|
||||
"hourShort": "小時",
|
||||
"dayShort": "天"
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "視覺化效果類型"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,6 +657,9 @@ if (mprisPlayer) {
|
||||
}
|
||||
currentState.volume = volume;
|
||||
broadcast({ data: volume, event: 'volume' });
|
||||
getMainWindow()?.webContents.send('request-volume', {
|
||||
volume,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,46 @@ import type { TitleTheme } from '/@/shared/types/types';
|
||||
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
|
||||
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) => {
|
||||
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: {
|
||||
'>=0.21.2': (store) => {
|
||||
store.set('window_bar_style', 'linux');
|
||||
},
|
||||
'>=1.0.0': (store) => {
|
||||
store.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -141,48 +141,48 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
||||
mprisPlayer.shuffle = shuffle;
|
||||
});
|
||||
|
||||
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
||||
try {
|
||||
if (!song?.id) {
|
||||
mprisPlayer.metadata = {};
|
||||
return;
|
||||
ipcMain.on(
|
||||
'update-song',
|
||||
(_event, song: QueueSong | undefined, imageUrl: null | string | undefined) => {
|
||||
try {
|
||||
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 };
|
||||
|
||||
+10
-1
@@ -30,6 +30,7 @@ import MenuBuilder from './menu';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
disableAutoUpdates,
|
||||
hotkeyToElectronAccelerator,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
@@ -456,7 +457,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
if (store.get('disable_auto_updates') !== true) {
|
||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||
new AppUpdater();
|
||||
}
|
||||
|
||||
@@ -702,3 +703,11 @@ if (!ipcMain.eventNames().includes('open-item')) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Register 'open-application-directory' handler globally, ensuring it is only registered once
|
||||
if (!ipcMain.eventNames().includes('open-application-directory')) {
|
||||
ipcMain.handle('open-application-directory', async () => {
|
||||
const userDataPath = app.getPath('userData');
|
||||
shell.openPath(userDataPath);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ if (process.env.NODE_ENV === 'development') {
|
||||
};
|
||||
}
|
||||
|
||||
export const disableAutoUpdates = () => {
|
||||
return process.env['DISABLE_AUTO_UPDATES'];
|
||||
};
|
||||
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
@@ -27,8 +27,8 @@ const updateShuffle = (shuffle: boolean) => {
|
||||
ipcRenderer.send('update-shuffle', shuffle);
|
||||
};
|
||||
|
||||
const updateSong = (song: QueueSong | undefined) => {
|
||||
ipcRenderer.send('update-song', song);
|
||||
const updateSong = (song: QueueSong | undefined, imageUrl?: null | string) => {
|
||||
ipcRenderer.send('update-song', song, imageUrl);
|
||||
};
|
||||
|
||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||
@@ -51,11 +51,16 @@ const requestToggleShuffle = (
|
||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||
};
|
||||
|
||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', cb);
|
||||
};
|
||||
|
||||
export const mpris = {
|
||||
requestPosition,
|
||||
requestSeek,
|
||||
requestToggleRepeat,
|
||||
requestToggleShuffle,
|
||||
requestVolume,
|
||||
updatePosition,
|
||||
updateRepeat,
|
||||
updateSeek,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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) => {
|
||||
return ipcRenderer.invoke('open-item', path);
|
||||
};
|
||||
|
||||
const openApplicationDirectory = async () => {
|
||||
return ipcRenderer.invoke('open-application-directory');
|
||||
};
|
||||
|
||||
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
|
||||
ipcRenderer.on('player-error-listener', cb);
|
||||
};
|
||||
@@ -36,12 +40,14 @@ const download = (url: string) => {
|
||||
};
|
||||
|
||||
export const utils = {
|
||||
disableAutoUpdates,
|
||||
download,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
logger,
|
||||
mainMessageListener,
|
||||
openApplicationDirectory,
|
||||
openItem,
|
||||
playerErrorListener,
|
||||
};
|
||||
|
||||
@@ -320,6 +320,20 @@ export const controller: GeneralController = {
|
||||
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) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -370,6 +384,20 @@ export const controller: GeneralController = {
|
||||
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) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -493,7 +521,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getRandomSongList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getRoles(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -535,7 +567,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSimilarSongs',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getSongDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -360,7 +360,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
},
|
||||
query: {
|
||||
...artistQuery,
|
||||
Fields: 'People, Tags',
|
||||
Fields: 'People, Tags, Studios',
|
||||
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
IsFavorite: query.favorite,
|
||||
@@ -426,10 +426,31 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).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) => {
|
||||
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 }) => {
|
||||
const userId = apiClientProps.server?.userId;
|
||||
@@ -670,6 +691,22 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
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) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
@@ -886,7 +923,12 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
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 {
|
||||
features,
|
||||
@@ -1077,9 +1119,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: items.map((item) =>
|
||||
jfNormalize.song(item, apiClientProps.server, query.imageSize),
|
||||
),
|
||||
items: items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount,
|
||||
};
|
||||
@@ -1093,7 +1133,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
const { bitrate, format, id, transcode } = query;
|
||||
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) {
|
||||
// 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?.[0];
|
||||
|
||||
const artistIds = hasFeature(apiClientProps.server, ServerFeature.BFR)
|
||||
? query.artistIds
|
||||
: query.artistIds?.[0];
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
artist_id: query.artistIds?.[0],
|
||||
artist_id: artistIds,
|
||||
compilation: query.compilation,
|
||||
genre_id: genres,
|
||||
has_rating: query.hasRating,
|
||||
@@ -401,6 +405,32 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).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,
|
||||
getFolder: SubsonicController.getFolder,
|
||||
getGenreList: async (args) => {
|
||||
@@ -461,6 +491,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
},
|
||||
getImageUrl: SubsonicController.getImageUrl,
|
||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
||||
getLyrics: SubsonicController.getLyrics,
|
||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||
@@ -664,9 +695,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((song) =>
|
||||
ndNormalize.song(song, apiClientProps.server, query.imageSize),
|
||||
),
|
||||
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)),
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AlbumDetailQuery,
|
||||
AlbumListQuery,
|
||||
ArtistListQuery,
|
||||
ArtistRadioQuery,
|
||||
FolderQuery,
|
||||
GenreListQuery,
|
||||
LyricSearchQuery,
|
||||
@@ -340,6 +341,10 @@ export const queryKeys: Record<
|
||||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
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) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
|
||||
@@ -201,6 +201,14 @@ export const contract = c.router({
|
||||
200: ssType._response.similarSongs,
|
||||
},
|
||||
},
|
||||
getSimilarSongs2: {
|
||||
method: 'GET',
|
||||
path: 'getSimilarSongs2',
|
||||
query: ssType._parameters.similarSongs2,
|
||||
responses: {
|
||||
200: ssType._response.similarSongs2,
|
||||
},
|
||||
},
|
||||
getSong: {
|
||||
method: 'GET',
|
||||
path: 'getSong.view',
|
||||
|
||||
@@ -155,7 +155,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).createFavorite({
|
||||
query: {
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -205,7 +208,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).removeFavorite({
|
||||
query: {
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -273,11 +279,11 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
) || null,
|
||||
};
|
||||
},
|
||||
@@ -297,7 +303,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
||||
|
||||
let results = artists.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
@@ -348,6 +354,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: query.startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
@@ -482,7 +489,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items:
|
||||
res.body.albumList2.album?.map((album) =>
|
||||
ssNormalize.album(album, apiClientProps.server, 300),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [],
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
@@ -503,6 +510,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
@@ -652,7 +660,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
let results = artists.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
@@ -676,6 +684,28 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
...args,
|
||||
query: { ...args.query, startIndex: 0 },
|
||||
}).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) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -821,6 +851,28 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
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) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
@@ -852,6 +904,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
||||
};
|
||||
},
|
||||
|
||||
getPlaylistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -867,7 +920,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||
},
|
||||
|
||||
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
@@ -1145,6 +1197,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
@@ -1289,6 +1342,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
@@ -1329,6 +1383,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
@@ -1432,6 +1487,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 1,
|
||||
songOffset: sectionIndex,
|
||||
@@ -1460,6 +1516,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
@@ -1729,6 +1786,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: query.albumStartIndex,
|
||||
artistCount: query.albumArtistLimit,
|
||||
artistOffset: query.albumArtistStartIndex,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.query,
|
||||
songCount: query.songLimit,
|
||||
songOffset: query.songStartIndex,
|
||||
|
||||
+8
-10
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import styles from './drag-preview.module.css';
|
||||
|
||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { DragData } from '/@/shared/types/drag-and-drop';
|
||||
@@ -23,22 +24,19 @@ const getItemName = (item: unknown): string => {
|
||||
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) => {
|
||||
const items = data.item || [];
|
||||
const { t } = useTranslation();
|
||||
const itemCount = items.length;
|
||||
const firstItem = items[0];
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -177,6 +177,7 @@
|
||||
}
|
||||
|
||||
.artist {
|
||||
width: 100%;
|
||||
color: white;
|
||||
text-shadow: 0 0 8px rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { MouseEvent } from '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 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 { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||
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 { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
||||
@@ -78,9 +78,15 @@ interface CarouselItemProps {
|
||||
}
|
||||
|
||||
const CarouselItem = ({ album }: CarouselItemProps) => {
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: album.imageId || undefined,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
type: 'itemCard',
|
||||
});
|
||||
|
||||
const { background: backgroundColor } = useFastAverageColor({
|
||||
algorithm: 'dominant',
|
||||
src: album.imageUrl || null,
|
||||
src: imageUrl || null,
|
||||
srcLoaded: true,
|
||||
});
|
||||
|
||||
@@ -110,10 +116,12 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
||||
</div>
|
||||
|
||||
<div className={styles.imageSection}>
|
||||
<Image
|
||||
<ItemImage
|
||||
className={styles.albumImage}
|
||||
containerClassName={styles.albumImageContainer}
|
||||
src={album.imageUrl || undefined}
|
||||
id={album.id}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
src={imageUrl}
|
||||
/>
|
||||
<div className={styles.playButtonOverlay}>
|
||||
<PlayButtonGroup onPlay={handlePlay} />
|
||||
@@ -123,7 +131,13 @@ const CarouselItem = ({ album }: CarouselItemProps) => {
|
||||
<div className={styles.metadataSection}>
|
||||
<Stack gap="sm">
|
||||
{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}
|
||||
</Text>
|
||||
)}
|
||||
@@ -201,28 +215,70 @@ export const FeatureCarousel = ({ data, onNearEnd }: FeatureCarouselProps) => {
|
||||
}
|
||||
}, [data, startIndex, itemsPerRow, onNearEnd]);
|
||||
|
||||
const handleNext = (e?: MouseEvent<HTMLButtonElement>) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (!data) return;
|
||||
directionRef.current = { isNext: true };
|
||||
setStartIndex((prev) => (prev + itemsPerRow) % data.length);
|
||||
};
|
||||
const handleNext = useCallback(
|
||||
(e?: MouseEvent<HTMLButtonElement>) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (!data) return;
|
||||
directionRef.current = { isNext: true };
|
||||
setStartIndex((prev) => (prev + itemsPerRow) % data.length);
|
||||
},
|
||||
[data, itemsPerRow],
|
||||
);
|
||||
|
||||
const handlePrevious = (e?: MouseEvent<HTMLButtonElement>) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (!data) return;
|
||||
directionRef.current = { isNext: false };
|
||||
setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length);
|
||||
};
|
||||
const handlePrevious = useCallback(
|
||||
(e?: MouseEvent<HTMLButtonElement>) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (!data) return;
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.carouselContainer} ref={containerRef}>
|
||||
<div className={styles.carouselContainer} onWheel={handleWheel} ref={containerRef}>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate="animate"
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Card {
|
||||
|
||||
interface GridCarouselProps {
|
||||
cards: Card[];
|
||||
enableRefresh?: boolean;
|
||||
hasNextPage?: boolean;
|
||||
loadNextPage?: () => void;
|
||||
onNextPage: (page: number) => void;
|
||||
@@ -46,6 +47,7 @@ const pageVariants: Variants = {
|
||||
function BaseGridCarousel(props: GridCarouselProps) {
|
||||
const {
|
||||
cards,
|
||||
enableRefresh = false,
|
||||
hasNextPage,
|
||||
loadNextPage,
|
||||
onNextPage,
|
||||
@@ -155,45 +157,65 @@ function BaseGridCarousel(props: GridCarouselProps) {
|
||||
{cq.isCalculated && (
|
||||
<>
|
||||
<div className={styles.navigation}>
|
||||
<Group gap="xs" justify="space-between" w="100%">
|
||||
<Group gap="xs">
|
||||
{typeof title === 'string' ? (
|
||||
{typeof title === 'string' ? (
|
||||
<Group gap="xs" justify="space-between" w="100%">
|
||||
<Group gap="xs">
|
||||
<TextTitle fw={700} isNoSelect order={3}>
|
||||
{title}
|
||||
</TextTitle>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
{onRefresh && (
|
||||
{enableRefresh && onRefresh && (
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'xs' }}
|
||||
onClick={onRefresh}
|
||||
size="xs"
|
||||
tooltip={{ label: 'Refresh' }}
|
||||
variant="transparent"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={onRefresh}
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
tooltip={{ label: 'Refresh' }}
|
||||
variant="transparent"
|
||||
variant="subtle"
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
) : (
|
||||
<div className={styles.customTitleContainer}>
|
||||
<div className={styles.customTitleContent}>{title}</div>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||
|
||||
@@ -32,6 +32,7 @@ interface ItemCardControlsProps {
|
||||
internalState?: ItemListStateActions;
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
itemType: LibraryItem;
|
||||
showRating: boolean;
|
||||
type?: 'compact' | 'default' | 'poster';
|
||||
}
|
||||
|
||||
@@ -180,6 +181,7 @@ export const ItemCardControls = ({
|
||||
internalState,
|
||||
item,
|
||||
itemType,
|
||||
showRating,
|
||||
type = 'default',
|
||||
}: ItemCardControlsProps) => {
|
||||
const playNowHandler = useMemo(
|
||||
@@ -267,6 +269,7 @@ export const ItemCardControls = ({
|
||||
<FavoriteButton isFavorite={isFavorite} onClick={favoriteHandler} />
|
||||
)}
|
||||
{controls?.onRating &&
|
||||
showRating &&
|
||||
(item?._serverType === ServerType.NAVIDROME ||
|
||||
item?._serverType === ServerType.SUBSONIC) && (
|
||||
<RatingButton
|
||||
|
||||
@@ -179,8 +179,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: var(--theme-spacing-xs);
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
background-color: alpha(var(--theme-colors-background), 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import clsx from 'clsx';
|
||||
import formatDuration from 'format-duration';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { Fragment, memo, ReactNode, useState } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import styles from './item-card.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
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 { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
|
||||
import {
|
||||
@@ -17,8 +18,16 @@ import {
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { useGeneralSettings } from '/@/renderer/store';
|
||||
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 { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
@@ -67,6 +76,7 @@ export const ItemCard = ({
|
||||
type = 'poster',
|
||||
withControls,
|
||||
}: ItemCardProps) => {
|
||||
const { showRatings } = useGeneralSettings();
|
||||
const imageUrl = getImageUrl(data);
|
||||
const rows = providedRows || [];
|
||||
|
||||
@@ -84,6 +94,7 @@ export const ItemCard = ({
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
rows={rows}
|
||||
showRating={showRatings}
|
||||
withControls={withControls}
|
||||
/>
|
||||
);
|
||||
@@ -100,6 +111,7 @@ export const ItemCard = ({
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
rows={rows}
|
||||
showRating={showRatings}
|
||||
withControls={withControls}
|
||||
/>
|
||||
);
|
||||
@@ -117,6 +129,7 @@ export const ItemCard = ({
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
rows={rows}
|
||||
showRating={showRatings}
|
||||
withControls={withControls}
|
||||
/>
|
||||
);
|
||||
@@ -130,18 +143,20 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
||||
imageUrl: string | undefined;
|
||||
internalState?: ItemListStateActions;
|
||||
rows: DataRow[];
|
||||
showRating: boolean;
|
||||
}
|
||||
|
||||
const CompactItemCard = ({
|
||||
controls,
|
||||
data,
|
||||
enableDrag,
|
||||
enableExpansion,
|
||||
enableNavigation,
|
||||
imageUrl,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
rows,
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
@@ -151,6 +166,53 @@ const CompactItemCard = ({
|
||||
: 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({
|
||||
onDoubleClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!data || !controls || !internalState) {
|
||||
@@ -239,7 +301,7 @@ const CompactItemCard = ({
|
||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||
? (data as { userRating: null | number }).userRating
|
||||
: null;
|
||||
const hasRating = userRating !== null && userRating > 0;
|
||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||
|
||||
const imageContainerClassName = clsx(styles.imageContainer, {
|
||||
[styles.isRound]: isRound,
|
||||
@@ -247,21 +309,25 @@ const CompactItemCard = ({
|
||||
|
||||
const imageContainerContent = (
|
||||
<>
|
||||
<Image
|
||||
<ItemImage
|
||||
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} />}
|
||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||
<AnimatePresence>
|
||||
{withControls && showControls && (
|
||||
{withControls && showControls && data && (
|
||||
<ItemCardControls
|
||||
controls={controls}
|
||||
enableExpansion={enableExpansion}
|
||||
internalState={internalState}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
showRating={hasRating}
|
||||
type="compact"
|
||||
/>
|
||||
)}
|
||||
@@ -288,8 +354,10 @@ const CompactItemCard = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.container, styles.compact, {
|
||||
[styles.dragging]: isDragging,
|
||||
[styles.selected]: isSelected,
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
{enableNavigation && navigationPath && !internalState ? (
|
||||
<Link
|
||||
@@ -351,11 +419,11 @@ const DefaultItemCard = ({
|
||||
data,
|
||||
enableExpansion,
|
||||
enableNavigation,
|
||||
imageUrl,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
rows,
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
@@ -457,13 +525,15 @@ const DefaultItemCard = ({
|
||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||
? (data as { userRating: null | number }).userRating
|
||||
: null;
|
||||
const hasRating = userRating !== null && userRating > 0;
|
||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||
|
||||
const imageContainerContent = (
|
||||
<>
|
||||
<Image
|
||||
<ItemImage
|
||||
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} />}
|
||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||
@@ -474,6 +544,7 @@ const DefaultItemCard = ({
|
||||
enableExpansion={enableExpansion}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
showRating={showRating}
|
||||
type="default"
|
||||
/>
|
||||
)}
|
||||
@@ -563,11 +634,11 @@ const PosterItemCard = ({
|
||||
enableDrag,
|
||||
enableExpansion,
|
||||
enableNavigation,
|
||||
imageUrl,
|
||||
internalState,
|
||||
isRound,
|
||||
itemType,
|
||||
rows,
|
||||
showRating,
|
||||
withControls,
|
||||
}: ItemCardDerivativeProps) => {
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
@@ -716,13 +787,15 @@ const PosterItemCard = ({
|
||||
typeof (data as { userRating: null | number }).userRating === 'number'
|
||||
? (data as { userRating: null | number }).userRating
|
||||
: null;
|
||||
const hasRating = userRating !== null && userRating > 0;
|
||||
const hasRating = showRating && userRating !== null && userRating > 0;
|
||||
|
||||
const imageContainerContent = (
|
||||
<>
|
||||
<Image
|
||||
<ItemImage
|
||||
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} />}
|
||||
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
|
||||
@@ -734,6 +807,7 @@ const PosterItemCard = ({
|
||||
internalState={internalState}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
showRating={showRating}
|
||||
type="poster"
|
||||
/>
|
||||
)}
|
||||
@@ -925,7 +999,7 @@ export const getDataRows = (): DataRow[] => {
|
||||
{
|
||||
format: (data) => {
|
||||
if ('duration' in data && data.duration !== null) {
|
||||
return formatDuration(data.duration * 1000);
|
||||
return formatDurationString(data.duration);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -943,7 +1017,7 @@ export const getDataRows = (): DataRow[] => {
|
||||
{
|
||||
format: (data) => {
|
||||
if ('releaseDate' in data && data.releaseDate) {
|
||||
return data.releaseDate;
|
||||
return formatDateAbsoluteUTC(data.releaseDate);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -961,7 +1035,12 @@ export const getDataRows = (): DataRow[] => {
|
||||
{
|
||||
format: (data) => {
|
||||
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 '';
|
||||
},
|
||||
@@ -970,7 +1049,7 @@ export const getDataRows = (): DataRow[] => {
|
||||
{
|
||||
format: (data) => {
|
||||
if ('playCount' in data && data.playCount !== null) {
|
||||
return String(data.playCount);
|
||||
return i18n.t('entity.play', { count: data.playCount });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -1019,7 +1098,7 @@ export const getDataRows = (): DataRow[] => {
|
||||
{
|
||||
format: (data) => {
|
||||
if ('songCount' in data && data.songCount !== null) {
|
||||
return String(data.songCount);
|
||||
return i18n.t('entity.trackWithCount', { count: data.songCount });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { MouseEvent, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
// import { AnimatePresence } from 'motion/react';
|
||||
// import { MouseEvent, useMemo, useState } from 'react';
|
||||
// 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 { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||
// import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
// import { Badge } from '/@/shared/components/badge/badge';
|
||||
// import { Divider } from '/@/shared/components/divider/divider';
|
||||
// import { Group } from '/@/shared/components/group/group';
|
||||
// import { Image } from '/@/shared/components/image/image';
|
||||
// import { Rating } from '/@/shared/components/rating/rating';
|
||||
// import { Text } from '/@/shared/components/text/text';
|
||||
// import {
|
||||
// Album,
|
||||
// AlbumArtist,
|
||||
// Artist,
|
||||
// LibraryItem,
|
||||
// Playlist,
|
||||
// Song,
|
||||
// } from '/@/shared/types/domain-types';
|
||||
// import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||
|
||||
interface ItemDetailProps {
|
||||
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
itemHeight: number;
|
||||
itemType: LibraryItem;
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||
withControls?: boolean;
|
||||
}
|
||||
// interface ItemDetailProps {
|
||||
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
// itemHeight: number;
|
||||
// itemType: LibraryItem;
|
||||
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||
// withControls?: boolean;
|
||||
// }
|
||||
|
||||
export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
||||
const imageUrl = getImageUrl(data);
|
||||
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
||||
// const imageUrl = getImageUrl(data);
|
||||
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
// const [showControls, setShowControls] = useState(false);
|
||||
|
||||
const { background } = useFastAverageColor({
|
||||
algorithm: 'simple',
|
||||
src: imageUrl,
|
||||
srcLoaded: false,
|
||||
});
|
||||
// const { background } = useFastAverageColor({
|
||||
// algorithm: 'simple',
|
||||
// src: imageUrl,
|
||||
// srcLoaded: false,
|
||||
// });
|
||||
|
||||
// const tags = [...(data?.genres ?? [])];
|
||||
// // const tags = [...(data?.genres ?? [])];
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
// const tags = useMemo(() => {
|
||||
// if (!data) {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
const items: {
|
||||
color?: string;
|
||||
id: string;
|
||||
isLight?: boolean;
|
||||
itemType: LibraryItem;
|
||||
name: string;
|
||||
}[] = [];
|
||||
// const items: {
|
||||
// color?: string;
|
||||
// id: string;
|
||||
// isLight?: boolean;
|
||||
// itemType: LibraryItem;
|
||||
// name: string;
|
||||
// }[] = [];
|
||||
|
||||
if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
||||
data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
||||
items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
||||
});
|
||||
}
|
||||
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
||||
// data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
||||
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
||||
// });
|
||||
// }
|
||||
|
||||
if ('genres' in data && Array.isArray(data.genres)) {
|
||||
data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
||||
const { color, isLight } = stringToColor(tag.name);
|
||||
items.push({ ...tag, color, isLight });
|
||||
});
|
||||
}
|
||||
// if ('genres' in data && Array.isArray(data.genres)) {
|
||||
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
||||
// const { color, isLight } = stringToColor(tag.name);
|
||||
// items.push({ ...tag, color, isLight });
|
||||
// });
|
||||
// }
|
||||
|
||||
// if ('tags' in data && typeof data.tags === 'object') {
|
||||
// console.log('data.tags :>> ', data.tags);
|
||||
// Object.entries(data.tags).forEach(([key, value]) => {
|
||||
// items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
||||
// });
|
||||
// }
|
||||
// // if ('tags' in data && typeof data.tags === 'object') {
|
||||
// // console.log('data.tags :>> ', data.tags);
|
||||
// // Object.entries(data.tags).forEach(([key, value]) => {
|
||||
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
||||
// // });
|
||||
// // }
|
||||
|
||||
return items;
|
||||
}, [data]);
|
||||
// return items;
|
||||
// }, [data]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={(e) => onClick?.(e, data, itemType)}
|
||||
style={{ backgroundColor: background }}
|
||||
>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
onMouseEnter={() => withControls && setShowControls(true)}
|
||||
onMouseLeave={() => withControls && setShowControls(false)}
|
||||
>
|
||||
<Image alt={data?.name} src={imageUrl} />
|
||||
<AnimatePresence>
|
||||
{withControls && showControls && <ItemCardControls type="compact" />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className={styles.metadataContainer}>
|
||||
<div className={styles.header}>
|
||||
<Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
||||
{data?.name}
|
||||
</Text>
|
||||
<Group>
|
||||
{data && 'userRating' in data && (
|
||||
<Rating size="xs" value={data?.userRating ?? 0} />
|
||||
)}
|
||||
{data && 'userFavorite' in data && (
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: data?.userFavorite ? 'primary' : 'default',
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={styles.content}>
|
||||
<Group className={styles.tags} gap="xs">
|
||||
{tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag.id}
|
||||
style={{
|
||||
backgroundColor: tag.color,
|
||||
color: tag.isLight ? 'black' : 'white',
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div
|
||||
// className={styles.container}
|
||||
// onClick={(e) => onClick?.(e, data, itemType)}
|
||||
// style={{ backgroundColor: background }}
|
||||
// >
|
||||
// <div
|
||||
// className={styles.imageContainer}
|
||||
// onMouseEnter={() => withControls && setShowControls(true)}
|
||||
// onMouseLeave={() => withControls && setShowControls(false)}
|
||||
// >
|
||||
// <Image alt={data?.name} src={imageUrl} />
|
||||
// <AnimatePresence>
|
||||
// {withControls && showControls && <ItemCardControls type="compact" />}
|
||||
// </AnimatePresence>
|
||||
// </div>
|
||||
// <div className={styles.metadataContainer}>
|
||||
// <div className={styles.header}>
|
||||
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
||||
// {data?.name}
|
||||
// </Text>
|
||||
// <Group>
|
||||
// {data && 'userRating' in data && (
|
||||
// <Rating size="xs" value={data?.userRating ?? 0} />
|
||||
// )}
|
||||
// {data && 'userFavorite' in data && (
|
||||
// <ActionIcon
|
||||
// icon="favorite"
|
||||
// iconProps={{
|
||||
// fill: data?.userFavorite ? 'primary' : 'default',
|
||||
// }}
|
||||
// size="xs"
|
||||
// />
|
||||
// )}
|
||||
// </Group>
|
||||
// </div>
|
||||
// <Divider />
|
||||
// <div className={styles.content}>
|
||||
// <Group className={styles.tags} gap="xs">
|
||||
// {tags.map((tag) => (
|
||||
// <Badge
|
||||
// key={tag.id}
|
||||
// style={{
|
||||
// backgroundColor: tag.color,
|
||||
// color: tag.isLight ? 'black' : 'white',
|
||||
// }}
|
||||
// >
|
||||
// {tag.name}
|
||||
// </Badge>
|
||||
// ))}
|
||||
// </Group>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
||||
if (data && 'imageUrl' in data) {
|
||||
return data.imageUrl || undefined;
|
||||
}
|
||||
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
||||
// if (data && 'imageUrl' in data) {
|
||||
// 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 { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const getQueryKeyName = (itemType: LibraryItem): string => {
|
||||
export const getListQueryKeyName = (itemType: LibraryItem): string => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return 'albums';
|
||||
@@ -115,7 +115,7 @@ export const useItemListInfiniteLoader = ({
|
||||
|
||||
return result;
|
||||
},
|
||||
queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
});
|
||||
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
@@ -186,10 +186,9 @@ export const useItemListInfiniteLoader = ({
|
||||
lastFetchedPageRef.current = -1;
|
||||
currentVisibleRangeRef.current = null;
|
||||
|
||||
// Invalidate and wait for count query to refetch (this will suspend via useSuspenseQuery)
|
||||
await queryClient.refetchQueries({
|
||||
// Invalidate and wait for count query to refetch
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: countQueryKey,
|
||||
type: 'active',
|
||||
});
|
||||
|
||||
// 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 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column !important;
|
||||
width: 100%;
|
||||
|
||||
@@ -68,6 +68,7 @@ interface VirtualizedGridListProps {
|
||||
outerRef: RefObject<any>;
|
||||
ref: RefObject<FixedSizeList<GridItemProps> | null>;
|
||||
rows?: ItemCardProps['rows'];
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
tableMetaRef: RefObject<null | {
|
||||
columnCount: number;
|
||||
itemHeight: number;
|
||||
@@ -95,6 +96,7 @@ const VirtualizedGridList = React.memo(
|
||||
outerRef,
|
||||
ref,
|
||||
rows,
|
||||
size,
|
||||
tableMetaRef,
|
||||
width,
|
||||
}: VirtualizedGridListProps) => {
|
||||
@@ -113,6 +115,7 @@ const VirtualizedGridList = React.memo(
|
||||
internalState,
|
||||
itemType,
|
||||
rows,
|
||||
size,
|
||||
tableMeta,
|
||||
};
|
||||
}, [
|
||||
@@ -126,6 +129,7 @@ const VirtualizedGridList = React.memo(
|
||||
gap,
|
||||
internalState,
|
||||
itemType,
|
||||
size,
|
||||
]);
|
||||
|
||||
const handleOnScroll = useCallback(
|
||||
@@ -215,7 +219,11 @@ const VirtualizedGridList = React.memo(
|
||||
|
||||
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) => {
|
||||
const isSm = width >= 600;
|
||||
const isMd = width >= 768;
|
||||
@@ -228,11 +236,11 @@ const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) =
|
||||
let dynamicItemsPerRow = 2;
|
||||
|
||||
if (is4xl) {
|
||||
dynamicItemsPerRow = 12;
|
||||
} else if (is3xl) {
|
||||
dynamicItemsPerRow = 10;
|
||||
} else if (is2xl) {
|
||||
} else if (is3xl) {
|
||||
dynamicItemsPerRow = 8;
|
||||
} else if (is2xl) {
|
||||
dynamicItemsPerRow = 7;
|
||||
} else if (isXl) {
|
||||
dynamicItemsPerRow = 6;
|
||||
} else if (isLg) {
|
||||
@@ -245,10 +253,22 @@ const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) =
|
||||
dynamicItemsPerRow = 2;
|
||||
}
|
||||
|
||||
if (size === 'large') {
|
||||
dynamicItemsPerRow = Math.round(dynamicItemsPerRow * 0.75);
|
||||
if (dynamicItemsPerRow < 1) {
|
||||
dynamicItemsPerRow = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
|
||||
|
||||
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) {
|
||||
return;
|
||||
@@ -273,6 +293,7 @@ export interface GridItemProps {
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
rows?: ItemCardProps['rows'];
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
tableMeta: null | {
|
||||
columnCount: number;
|
||||
itemHeight: number;
|
||||
@@ -286,6 +307,7 @@ export interface ItemGridListProps {
|
||||
enableDrag?: boolean;
|
||||
enableExpansion?: boolean;
|
||||
enableSelection?: boolean;
|
||||
enableSelectionDialog?: boolean;
|
||||
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||
getRowId?: ((item: unknown) => string) | string;
|
||||
initialTop?: {
|
||||
@@ -300,6 +322,7 @@ export interface ItemGridListProps {
|
||||
overrideControls?: Partial<ItemControls>;
|
||||
ref?: Ref<ItemListHandle>;
|
||||
rows?: ItemCardProps['rows'];
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
}
|
||||
|
||||
const BaseItemGridList = ({
|
||||
@@ -319,6 +342,7 @@ const BaseItemGridList = ({
|
||||
overrideControls,
|
||||
ref,
|
||||
rows,
|
||||
size = 'default',
|
||||
}: ItemGridListProps) => {
|
||||
const rootRef = useRef(null);
|
||||
const outerRef = useRef(null);
|
||||
@@ -409,8 +433,8 @@ const BaseItemGridList = ({
|
||||
}, [osInstance]);
|
||||
|
||||
const throttledSetTableMeta = useMemo(() => {
|
||||
return createThrottledSetTableMeta(itemsPerRow, rows?.length);
|
||||
}, [itemsPerRow, rows?.length]);
|
||||
return createThrottledSetTableMeta(itemsPerRow, rows?.length, size);
|
||||
}, [itemsPerRow, rows?.length, size]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { current: container } = containerRef;
|
||||
@@ -737,19 +761,23 @@ const BaseItemGridList = ({
|
||||
outerRef={outerRef}
|
||||
ref={listRef}
|
||||
rows={rows}
|
||||
size={size}
|
||||
tableMetaRef={tableMetaRef}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
||||
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 itemCount = data.length;
|
||||
@@ -780,6 +808,7 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
||||
internalState={props.data.internalState}
|
||||
itemType={itemType}
|
||||
rows={rows}
|
||||
type={size === 'compact' ? 'compact' : 'poster'}
|
||||
withControls
|
||||
/>
|
||||
</div>,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import styles from './image-column.module.css';
|
||||
|
||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||
import {
|
||||
ItemTableListInnerColumn,
|
||||
TableColumnContainer,
|
||||
@@ -14,17 +15,14 @@ import {
|
||||
} from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Folder, LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const ImageColumn = (props: ItemTableListInnerColumn) => {
|
||||
const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.[
|
||||
props.columns[props.columnIndex].id
|
||||
];
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const row: string | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id;
|
||||
const item = props.data[props.rowIndex] as any;
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const internalState = (props as any).internalState;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
@@ -80,12 +78,14 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => {
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Image
|
||||
<ItemImage
|
||||
containerClassName={clsx({
|
||||
[styles.imageContainerWithAspectRatio]:
|
||||
props.size === 'default' || props.size === 'large',
|
||||
})}
|
||||
src={row}
|
||||
id={item?.id}
|
||||
itemType={item?._itemType}
|
||||
src={item?.imageUrl}
|
||||
/>
|
||||
{isHovered && (
|
||||
<div
|
||||
|
||||
+19
-9
@@ -4,6 +4,7 @@ import { generatePath, Link } from 'react-router';
|
||||
|
||||
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 {
|
||||
ColumnNullFallback,
|
||||
@@ -19,13 +20,12 @@ import {
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
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 internalState = (props as any).internalState;
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
@@ -74,8 +74,8 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
||||
};
|
||||
|
||||
const artists = useMemo(() => {
|
||||
if (row && 'artists' in row && Array.isArray(row.artists)) {
|
||||
return (row.artists as RelatedAlbumArtist[]).map((artist) => {
|
||||
if (row && 'artists' in item && Array.isArray(item.artists)) {
|
||||
return (item.artists as RelatedAlbumArtist[]).map((artist) => {
|
||||
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
||||
artistId: artist.id,
|
||||
});
|
||||
@@ -83,9 +83,9 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
||||
});
|
||||
}
|
||||
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 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)}
|
||||
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 && (
|
||||
<div
|
||||
className={clsx(styles.playButtonOverlay, {
|
||||
@@ -138,7 +143,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
||||
})}
|
||||
>
|
||||
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
|
||||
{row.name as string}
|
||||
{item.name as string}
|
||||
</Text>
|
||||
<div className={styles.artists}>
|
||||
{artists.map((artist, index) => (
|
||||
@@ -263,7 +268,12 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
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 && (
|
||||
<div
|
||||
className={clsx(styles.playButtonOverlay, {
|
||||
|
||||
@@ -18,34 +18,34 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
width: 300,
|
||||
},
|
||||
@@ -61,7 +61,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM,
|
||||
@@ -70,7 +70,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
@@ -115,7 +115,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
@@ -178,7 +178,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
@@ -214,7 +214,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.DATE_ADDED,
|
||||
@@ -232,7 +232,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
@@ -252,7 +252,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
},
|
||||
@@ -268,9 +268,9 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
},
|
||||
@@ -284,34 +284,34 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.IMAGE,
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: false,
|
||||
isEnabled: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
||||
pinned: 'left',
|
||||
pinned: null,
|
||||
value: TableColumn.TITLE_COMBINED,
|
||||
width: 300,
|
||||
},
|
||||
@@ -327,7 +327,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'start',
|
||||
autoSize: true,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ALBUM_ARTIST,
|
||||
@@ -381,7 +381,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.RELEASE_DATE,
|
||||
@@ -390,7 +390,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
@@ -399,7 +399,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.DATE_ADDED,
|
||||
@@ -408,7 +408,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.PLAY_COUNT,
|
||||
@@ -419,7 +419,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
},
|
||||
@@ -435,9 +435,9 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
},
|
||||
@@ -451,7 +451,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
@@ -539,7 +539,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.USER_FAVORITE,
|
||||
width: 60,
|
||||
},
|
||||
@@ -555,9 +555,9 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
},
|
||||
@@ -571,7 +571,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
@@ -630,9 +630,9 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
},
|
||||
@@ -646,7 +646,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||
pinned: null,
|
||||
value: TableColumn.ROW_INDEX,
|
||||
width: 80,
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
align: 'start',
|
||||
@@ -678,9 +678,9 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
||||
{
|
||||
align: 'center',
|
||||
autoSize: false,
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
|
||||
pinned: 'right',
|
||||
pinned: null,
|
||||
value: TableColumn.ACTIONS,
|
||||
width: 60,
|
||||
},
|
||||
@@ -728,6 +728,15 @@ export const pickTableColumns = (options: {
|
||||
const enabledSet = new Set(enabledColumns);
|
||||
const remaining = columns.filter((col) => !enabledSet.has(col.value));
|
||||
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 {
|
||||
columnsToProcess = columns;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.item-table-list-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
@@ -672,6 +672,7 @@ interface ItemTableListProps {
|
||||
enableHorizontalBorders?: boolean;
|
||||
enableRowHoverHighlight?: boolean;
|
||||
enableSelection?: boolean;
|
||||
enableSelectionDialog?: boolean;
|
||||
enableStickyGroupRows?: boolean;
|
||||
enableStickyHeader?: boolean;
|
||||
enableVerticalBorders?: boolean;
|
||||
@@ -2318,6 +2319,7 @@ const BaseItemTableList = ({
|
||||
totalRowCount={totalRowCount}
|
||||
/>
|
||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||
</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> {
|
||||
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||
itemsPerRow?: number;
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
}
|
||||
|
||||
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 { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
|
||||
interface NativeScrollAreaProps {
|
||||
@@ -26,35 +27,31 @@ const BaseNativeScrollArea = forwardRef(
|
||||
const { windowBarStyle } = useWindowSettings();
|
||||
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({
|
||||
defer: false,
|
||||
events: {
|
||||
scroll: (_instance, e) => {
|
||||
if (scrollHandlerRef.current) {
|
||||
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');
|
||||
}
|
||||
});
|
||||
scrollHandler(e);
|
||||
},
|
||||
},
|
||||
options: {
|
||||
|
||||
@@ -37,6 +37,18 @@
|
||||
input {
|
||||
-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 {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export type AutoDJQueueAddedEventPayload = {
|
||||
songCount: number;
|
||||
};
|
||||
|
||||
export type EventMap = {
|
||||
AUTODJ_QUEUE_ADDED: AutoDJQueueAddedEventPayload;
|
||||
ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
|
||||
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
|
||||
MEDIA_NEXT: MediaNextEventPayload;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ServerRequired = () => {
|
||||
|
||||
const isServerLock = Boolean(window.SERVER_LOCK) || false;
|
||||
|
||||
if (Object.keys(serverList).length > 1) {
|
||||
if (Object.keys(serverList).length > 0) {
|
||||
return (
|
||||
<ScrollArea>
|
||||
<Stack miw="300px">
|
||||
|
||||
@@ -23,8 +23,8 @@ const NoNetworkRoute = () => {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<PageHeader />
|
||||
<Center style={{ height: '100%', width: '100vw' }}>
|
||||
<Stack gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Stack align="center" gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
|
||||
<Icon icon="wifiOff" size="4rem" />
|
||||
<Stack gap="md">
|
||||
<Text size="xl" weight={600}>
|
||||
|
||||
@@ -42,6 +42,7 @@ import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||
import {
|
||||
Album,
|
||||
@@ -88,6 +89,12 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
||||
items.push(...releaseTypes);
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'isCompilation',
|
||||
value: album?.isCompilation
|
||||
? t('filter.isCompilation', { postProcess: 'sentenceCase' })
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'releaseDate',
|
||||
value: album.releaseDate
|
||||
@@ -96,7 +103,11 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
||||
},
|
||||
{
|
||||
id: 'releaseYear',
|
||||
value: album.releaseYear?.toString(),
|
||||
value: album.releaseDate
|
||||
? undefined
|
||||
: album.releaseYear
|
||||
? album.releaseYear.toString()
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
@@ -136,19 +147,7 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: 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',
|
||||
value: album.version || undefined,
|
||||
@@ -342,6 +341,7 @@ export const AlbumDetailContent = () => {
|
||||
uniqueId: 'moreFromArtist',
|
||||
},
|
||||
{
|
||||
enableRefresh: true,
|
||||
excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined,
|
||||
isHidden: !detailQuery?.data?.genres?.[0],
|
||||
query: {
|
||||
@@ -362,6 +362,9 @@ export const AlbumDetailContent = () => {
|
||||
|
||||
const comment = detailQuery?.data?.comment;
|
||||
|
||||
const releaseYear = detailQuery?.data?.releaseYear;
|
||||
const labels = detailQuery?.data?.recordLabels;
|
||||
|
||||
const mbzId = detailQuery?.data?.mbzId;
|
||||
|
||||
return (
|
||||
@@ -369,9 +372,7 @@ export const AlbumDetailContent = () => {
|
||||
<div className={styles.detailContainer}>
|
||||
{comment && (
|
||||
<Spoiler maxHeight={75}>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: replaceURLWithHTMLLinks(comment) }}
|
||||
/>
|
||||
<Text pb="md">{replaceURLWithHTMLLinks(comment)}</Text>
|
||||
</Spoiler>
|
||||
)}
|
||||
<div className={styles.contentLayout}>
|
||||
@@ -396,7 +397,15 @@ export const AlbumDetailContent = () => {
|
||||
</Stack>
|
||||
</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">
|
||||
{cq.height || cq.width ? (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
@@ -404,6 +413,7 @@ export const AlbumDetailContent = () => {
|
||||
.filter((c) => !c.isHidden)
|
||||
.map((carousel) => (
|
||||
<AlbumInfiniteCarousel
|
||||
enableRefresh={carousel.enableRefresh}
|
||||
excludeIds={carousel.excludeIds}
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
query={carousel.query}
|
||||
@@ -428,6 +438,7 @@ interface AlbumDetailSongsTableProps {
|
||||
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table);
|
||||
|
||||
const currentSong = usePlayerSong();
|
||||
@@ -441,11 +452,11 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
|
||||
const filteredSongs = useMemo(() => {
|
||||
return sortSongList(
|
||||
searchLibraryItems(songs, searchTerm, LibraryItem.SONG),
|
||||
searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
);
|
||||
}, [songs, searchTerm, sortBy, sortOrder]);
|
||||
}, [songs, debouncedSearchTerm, sortBy, sortOrder]);
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.ALBUM_DETAIL,
|
||||
@@ -493,7 +504,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
|
||||
const groups = useMemo(() => {
|
||||
// Remove groups when filtering
|
||||
if (searchTerm.trim()) {
|
||||
if (debouncedSearchTerm.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -579,7 +590,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
},
|
||||
rowHeight: 40,
|
||||
}));
|
||||
}, [searchTerm, sortBy, discGroups, t]);
|
||||
}, [debouncedSearchTerm, sortBy, discGroups, t]);
|
||||
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -677,6 +688,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableStickyGroupRows
|
||||
enableStickyHeader
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { generatePath, Link, useParams } from 'react-router';
|
||||
|
||||
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 { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
LibraryHeaderMenu,
|
||||
} from '/@/renderer/features/shared/components/library-header';
|
||||
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 { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
@@ -23,13 +24,15 @@ import { Play } from '/@/shared/types/types';
|
||||
export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const server = useCurrentServer();
|
||||
const { showRatings } = useGeneralSettings();
|
||||
const detailQuery = useQuery(
|
||||
albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
|
||||
);
|
||||
|
||||
const showRating =
|
||||
detailQuery?.data?._serverType === ServerType.NAVIDROME ||
|
||||
detailQuery?.data?._serverType === ServerType.SUBSONIC;
|
||||
showRatings &&
|
||||
(detailQuery?.data?._serverType === ServerType.NAVIDROME ||
|
||||
detailQuery?.data?._serverType === ServerType.SUBSONIC);
|
||||
|
||||
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
@@ -82,10 +85,16 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
const firstAlbumArtist = detailQuery?.data?.albumArtists?.[0];
|
||||
const releaseYear = detailQuery?.data?.releaseYear;
|
||||
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: detailQuery?.data?.imageId || undefined,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
type: 'header',
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack ref={ref}>
|
||||
<LibraryHeader
|
||||
imageUrl={detailQuery?.data?.imageUrl}
|
||||
imageUrl={imageUrl}
|
||||
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface AlbumCarouselProps {
|
||||
enableRefresh?: boolean;
|
||||
excludeIds?: string[];
|
||||
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
||||
rowCount?: number;
|
||||
@@ -28,7 +29,15 @@ interface 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 {
|
||||
data: albums,
|
||||
@@ -81,6 +90,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
||||
return (
|
||||
<GridCarousel
|
||||
cards={cards}
|
||||
enableRefresh={enableRefresh}
|
||||
hasNextPage={hasNextPage}
|
||||
loadNextPage={fetchNextPage}
|
||||
onNextPage={handleNextPage}
|
||||
|
||||
@@ -87,8 +87,9 @@ export const AlbumListView = ({
|
||||
table,
|
||||
}: ItemListSettings & { overrideQuery?: OverrideAlbumListQuery }) => {
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useListContext();
|
||||
|
||||
const { query } = useAlbumListFilters();
|
||||
const { query } = useAlbumListFilters(pageKey as ItemListKey);
|
||||
|
||||
const mergedQuery = useMemo(() => {
|
||||
if (!overrideQuery) {
|
||||
@@ -114,6 +115,7 @@ export const AlbumListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -125,6 +127,7 @@ export const AlbumListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
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 { useListContext } from '/@/renderer/context/list-context';
|
||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||
@@ -22,17 +23,13 @@ interface AlbumListHeaderProps {
|
||||
}
|
||||
|
||||
export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
|
||||
const { itemCount } = useListContext();
|
||||
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar ignoreMaxWidth>
|
||||
<PlayButton />
|
||||
<PageTitle title={title} />
|
||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
<AlbumListHeaderBadge />
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<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 { t } = useTranslation();
|
||||
const { pageKey } = useListContext();
|
||||
|
||||
@@ -27,6 +27,7 @@ export const AlbumListInfiniteGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: AlbumListInfiniteGridProps) => {
|
||||
const listCountQuery = albumQueries.listCount({
|
||||
query: { ...query },
|
||||
@@ -65,6 +66,7 @@ export const AlbumListInfiniteGrid = ({
|
||||
onRangeChanged={onRangeChanged}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const AlbumListPaginatedGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: AlbumListPaginatedGridProps) => {
|
||||
const listCountQuery = albumQueries.listCount({
|
||||
query: { ...query },
|
||||
@@ -77,6 +78,7 @@ export const AlbumListPaginatedGrid = ({
|
||||
itemType={LibraryItem.ALBUM}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Fragment, Suspense, useCallback, useRef } from 'react';
|
||||
|
||||
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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import {
|
||||
@@ -197,10 +198,16 @@ export const ExpandedAlbumListItem = ({ internalState, item }: ExpandedAlbumList
|
||||
|
||||
const player = usePlayer();
|
||||
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: item.imageId || undefined,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
type: 'itemCard',
|
||||
});
|
||||
|
||||
const color = useFastAverageColor({
|
||||
algorithm: 'sqrt',
|
||||
id: item.id,
|
||||
src: data?.imageUrl,
|
||||
src: imageUrl,
|
||||
srcLoaded: true,
|
||||
});
|
||||
|
||||
@@ -300,7 +307,7 @@ export const ExpandedAlbumListItem = ({ internalState, item }: ExpandedAlbumList
|
||||
className={styles.backgroundImage}
|
||||
style={{
|
||||
['--bg-color' as string]: color?.background,
|
||||
backgroundImage: `url(${data?.imageUrl})`,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
}}
|
||||
/>
|
||||
{data?.songs && data.songs.length > 0 && (
|
||||
|
||||
@@ -2,10 +2,7 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
MultiSelectWithInvalidData,
|
||||
SelectWithInvalidData,
|
||||
} from '/@/renderer/components/select-with-invalid-data';
|
||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||
@@ -187,14 +184,14 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
<SelectWithInvalidData
|
||||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
data={selectableAlbumArtists}
|
||||
defaultValue={query.artistIds?.[0] || undefined}
|
||||
defaultValue={query.artistIds || []}
|
||||
disabled={disableArtistFilter}
|
||||
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
|
||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||
limit={300}
|
||||
onChange={(e) => setAlbumArtist(e ? [e] : null)}
|
||||
onChange={(e) => (e && e.length > 0 ? setAlbumArtist(e) : setAlbumArtist(null))}
|
||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||
searchable
|
||||
/>
|
||||
@@ -206,10 +203,10 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
|
||||
|
||||
interface TagFilterItemProps {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
onChange: (value: null | string[]) => void;
|
||||
options: Array<{ id: string; name: string }>;
|
||||
tagValue: string;
|
||||
value: string | undefined;
|
||||
value: string | string[] | undefined;
|
||||
}
|
||||
|
||||
const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterItemProps) => {
|
||||
@@ -222,15 +219,20 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
|
||||
[options],
|
||||
);
|
||||
|
||||
const defaultValue = useMemo(() => {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<SelectWithInvalidData
|
||||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
data={selectData}
|
||||
defaultValue={value}
|
||||
defaultValue={defaultValue}
|
||||
key={tagValue}
|
||||
label={label}
|
||||
limit={100}
|
||||
onChange={onChange}
|
||||
onChange={(e) => (e && e.length > 0 ? onChange(e) : onChange(null))}
|
||||
searchable
|
||||
/>
|
||||
);
|
||||
@@ -257,7 +259,7 @@ const TagFilters = () => {
|
||||
);
|
||||
|
||||
const handleTagFilter = useMemo(
|
||||
() => (tag: string, e: null | string) => {
|
||||
() => (tag: string, e: null | string[]) => {
|
||||
setCustom({ [tag]: e });
|
||||
},
|
||||
[setCustom],
|
||||
@@ -289,7 +291,7 @@ const TagFilters = () => {
|
||||
onChange={(e) => handleTagFilter(tag.value, e)}
|
||||
options={tag.options}
|
||||
tagValue={tag.value}
|
||||
value={query._custom?.[tag.value] as string | undefined}
|
||||
value={query._custom?.[tag.value] as string | string[] | undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -15,13 +15,15 @@ import {
|
||||
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
export const useAlbumListFilters = () => {
|
||||
export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
||||
const resolvedListKey = listKey ?? ItemListKey.ALBUM;
|
||||
|
||||
const { setSortBy, sortBy } = useSortByFilter<AlbumListSort>(
|
||||
AlbumListSort.NAME,
|
||||
ItemListKey.ALBUM,
|
||||
resolvedListKey,
|
||||
);
|
||||
|
||||
const { setSortOrder, sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.ALBUM);
|
||||
const { setSortOrder, sortOrder } = useSortOrderFilter(SortOrder.ASC, resolvedListKey);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
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 { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
|
||||
@@ -34,9 +35,16 @@ const AlbumDetailRoute = () => {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const imageUrl =
|
||||
useItemImageUrl({
|
||||
id: detailQuery?.data?.imageId || undefined,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
type: 'itemCard',
|
||||
}) || '';
|
||||
|
||||
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
|
||||
id: albumId,
|
||||
src: detailQuery.data?.imageUrl,
|
||||
src: imageUrl,
|
||||
srcLoaded: !detailQuery.isLoading,
|
||||
});
|
||||
|
||||
@@ -45,7 +53,7 @@ const AlbumDetailRoute = () => {
|
||||
const showBlurredImage = albumBackground;
|
||||
|
||||
const { isReady } = useWaitForColorCalculation({
|
||||
hasImage: !!detailQuery.data?.imageUrl,
|
||||
hasImage: !!imageUrl,
|
||||
isLoading: isColorLoading,
|
||||
routeId: albumId,
|
||||
showBlurredImage,
|
||||
@@ -81,7 +89,7 @@ const AlbumDetailRoute = () => {
|
||||
<LibraryBackgroundImage
|
||||
blur={albumBackgroundBlur}
|
||||
headerRef={headerRef}
|
||||
imageUrl={detailQuery.data?.imageUrl}
|
||||
imageUrl={imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from './dummy-album-detail-route.module.css';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||
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 (
|
||||
<AnimatedPage key={`dummy-album-detail-${albumId}`}>
|
||||
<LibraryContainer>
|
||||
<Stack>
|
||||
<LibraryHeader
|
||||
imageUrl={detailQuery?.data?.imageUrl}
|
||||
imageUrl={imageUrl}
|
||||
item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }}
|
||||
loading={!background || colorId !== albumId}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
@@ -212,11 +219,7 @@ const DummyAlbumDetailRoute = () => {
|
||||
{comment && (
|
||||
<section>
|
||||
<Spoiler maxHeight={75}>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceURLWithHTMLLinks(comment),
|
||||
}}
|
||||
/>
|
||||
<Text pb="md">{replaceURLWithHTMLLinks(comment)}</Text>
|
||||
</Spoiler>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -127,15 +127,19 @@ const getPlayerProperties = (): Pick<
|
||||
const playbackSettings = useSettingsStore.getState().playback;
|
||||
|
||||
return {
|
||||
'player.mediaSession': playbackSettings.mediaSession,
|
||||
'player.mediaSession': ignoreWeb(playbackSettings.mediaSession),
|
||||
'player.queueType': player.player.queueType,
|
||||
'player.style': player.player.transitionType,
|
||||
'player.transcoding': playbackSettings.transcode.enabled,
|
||||
'player.type': playbackSettings.type,
|
||||
'player.webAudio': playbackSettings.webAudio,
|
||||
};
|
||||
'player.type': ignoreWeb(playbackSettings.type),
|
||||
'player.webAudio': ignoreWeb(playbackSettings.webAudio),
|
||||
} as any;
|
||||
};
|
||||
|
||||
function ignoreWeb<T>(value: T): T | undefined {
|
||||
return isElectron() ? value : undefined;
|
||||
}
|
||||
|
||||
const getSettingsProperties = (): SettingsProperties => {
|
||||
const settings = useSettingsStore.getState();
|
||||
|
||||
@@ -148,22 +152,30 @@ const getSettingsProperties = (): SettingsProperties => {
|
||||
'settings.autoDJItemCount': settings.autoDJ.itemCount,
|
||||
'settings.autoDJTiming': settings.autoDJ.timing,
|
||||
'settings.customCss': settings.css.enabled,
|
||||
'settings.disableAutoUpdate': settings.window.disableAutoUpdate,
|
||||
'settings.discord': settings.discord.enabled,
|
||||
'settings.exitToTray': settings.window.exitToTray,
|
||||
'settings.disableAutoUpdate': ignoreWeb(settings.window.disableAutoUpdate),
|
||||
'settings.discord': ignoreWeb(settings.discord.enabled),
|
||||
'settings.exitToTray': ignoreWeb(settings.window.exitToTray),
|
||||
'settings.followSystemTheme': settings.general.followSystemTheme,
|
||||
'settings.fontType': settings.font.type,
|
||||
'settings.globalHotkeys': settings.hotkeys.globalMediaHotkeys,
|
||||
'settings.homeFeature': settings.general.homeFeature,
|
||||
'settings.language': settings.general.language,
|
||||
// 'settings.lastFM': settings.general.lastFM,
|
||||
'settings.lyrics.enableAutoTranslation': settings.lyrics.enableAutoTranslation,
|
||||
'settings.lyrics.enableNeteaseTranslation': settings.lyrics.enableNeteaseTranslation,
|
||||
'settings.lyrics.fetch': settings.lyrics.fetch,
|
||||
'settings.lyrics.sources.genius': settings.lyrics.sources.includes(LyricSource.GENIUS),
|
||||
'settings.lyrics.sources.lrclib': settings.lyrics.sources.includes(LyricSource.LRCLIB),
|
||||
'settings.lyrics.sources.netease': settings.lyrics.sources.includes(LyricSource.NETEASE),
|
||||
'settings.minimizeToTray': settings.window.minimizeToTray,
|
||||
'settings.lyrics.enableAutoTranslation': ignoreWeb(settings.lyrics.enableAutoTranslation),
|
||||
'settings.lyrics.enableNeteaseTranslation': ignoreWeb(
|
||||
settings.lyrics.enableNeteaseTranslation,
|
||||
),
|
||||
'settings.lyrics.fetch': ignoreWeb(settings.lyrics.fetch),
|
||||
'settings.lyrics.sources.genius': ignoreWeb(
|
||||
settings.lyrics.sources.includes(LyricSource.GENIUS),
|
||||
),
|
||||
'settings.lyrics.sources.lrclib': ignoreWeb(
|
||||
settings.lyrics.sources.includes(LyricSource.LRCLIB),
|
||||
),
|
||||
'settings.lyrics.sources.netease': ignoreWeb(
|
||||
settings.lyrics.sources.includes(LyricSource.NETEASE),
|
||||
),
|
||||
'settings.minimizeToTray': ignoreWeb(settings.window.minimizeToTray),
|
||||
// 'settings.musicBrainz': settings.general.musicBrainz,
|
||||
'settings.nativeAspectRatio': settings.general.nativeAspectRatio,
|
||||
'settings.playerbarSliderType': settings.general.playerbarSlider
|
||||
@@ -172,26 +184,26 @@ const getSettingsProperties = (): SettingsProperties => {
|
||||
// 'settings.playerbarWaveformBarWidth': settings.general.playerbarSlider.barWidth,
|
||||
// 'settings.playerbarWaveformGap': settings.general.playerbarSlider.barGap,
|
||||
// 'settings.playerbarWaveformRadius': settings.general.playerbarSlider.barRadius,
|
||||
'settings.preventSleepOnPlayback': settings.window.preventSleepOnPlayback,
|
||||
'settings.releaseChannel': settings.window.releaseChannel,
|
||||
'settings.preventSleepOnPlayback': ignoreWeb(settings.window.preventSleepOnPlayback),
|
||||
'settings.releaseChannel': ignoreWeb(settings.window.releaseChannel),
|
||||
'settings.resume': settings.general.resume,
|
||||
'settings.scrobble.enabled': settings.playback.scrobble.enabled,
|
||||
'settings.scrobble.notify': settings.playback.scrobble.notify,
|
||||
'settings.scrobble.notify': ignoreWeb(settings.playback.scrobble.notify),
|
||||
'settings.showLyricsInSidebar': settings.general.showLyricsInSidebar,
|
||||
'settings.showVisualizerInSidebar': settings.general.showVisualizerInSidebar,
|
||||
'settings.sideQueueType': settings.general.sideQueueType,
|
||||
// 'settings.skipBackwardSeconds': settings.general.skipButtons.skipBackwardSeconds,
|
||||
'settings.skipButtons': settings.general.skipButtons.enabled,
|
||||
// 'settings.skipForwardSeconds': settings.general.skipButtons.skipForwardSeconds,
|
||||
'settings.startMinimized': settings.window.startMinimized,
|
||||
'settings.startMinimized': ignoreWeb(settings.window.startMinimized),
|
||||
'settings.theme': settings.general.theme,
|
||||
'settings.themeDark': settings.general.themeDark,
|
||||
'settings.themeLight': settings.general.themeLight,
|
||||
'settings.tray': settings.window.tray,
|
||||
'settings.tray': ignoreWeb(settings.window.tray),
|
||||
'settings.useThemeAccentColor': settings.general.useThemeAccentColor,
|
||||
'settings.windowBarStyle': settings.window.windowBarStyle,
|
||||
'settings.zoomFactor': settings.general.zoomFactor,
|
||||
};
|
||||
'settings.windowBarStyle': ignoreWeb(settings.window.windowBarStyle),
|
||||
'settings.zoomFactor': ignoreWeb(settings.general.zoomFactor),
|
||||
} as any;
|
||||
};
|
||||
|
||||
const getServer = (): 'unknown' | ServerType => {
|
||||
@@ -202,6 +214,7 @@ const getServer = (): 'unknown' | ServerType => {
|
||||
|
||||
export const useAppTracker = () => {
|
||||
const { mutate: trackAppMutation } = useMutation(appTrackerMutation);
|
||||
const { mutate: trackAppViewMutation } = useMutation(appViewMutation);
|
||||
const hasRunOnMountRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -246,6 +259,10 @@ export const useAppTracker = () => {
|
||||
meta: { properties, todayUTC },
|
||||
});
|
||||
|
||||
trackAppViewMutation(undefined, {
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
trackAppMutation(properties, {
|
||||
onError: () => {},
|
||||
onSettled: () => {
|
||||
@@ -275,9 +292,10 @@ export const useAppTracker = () => {
|
||||
const interval = setInterval(checkAndTrack, 1000 * 60 * 60);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [trackAppMutation]);
|
||||
}, [trackAppMutation, trackAppViewMutation]);
|
||||
};
|
||||
|
||||
// Sends the app event to the analytics server which includes usage data
|
||||
const appTrackerMutation = mutationOptions({
|
||||
mutationFn: (properties: AppTrackerProperties) => {
|
||||
try {
|
||||
@@ -296,3 +314,24 @@ const appTrackerMutation = mutationOptions({
|
||||
retry: false,
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
// Sends a view event to the analytics server which only includes language, screen, and website
|
||||
// and triggers a page view event
|
||||
const appViewMutation = mutationOptions({
|
||||
mutationFn: () => {
|
||||
try {
|
||||
window.umami?.track((props) => ({
|
||||
language: props.language,
|
||||
screen: props.screen,
|
||||
website: props.website,
|
||||
}));
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
mutationKey: ['analytics', 'app-view'],
|
||||
onSuccess: () => {},
|
||||
retry: false,
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
@@ -10,3 +10,57 @@
|
||||
gap: var(--theme-spacing-2xl);
|
||||
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 { forwardRef, Fragment, Ref } from 'react';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
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 { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
LibraryHeaderMenu,
|
||||
} from '/@/renderer/features/shared/components/library-header';
|
||||
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 { formatDurationString } from '/@/renderer/utils';
|
||||
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 server = useCurrentServer();
|
||||
const { showRatings } = useGeneralSettings();
|
||||
const { t } = useTranslation();
|
||||
const detailQuery = useQuery(
|
||||
const detailQuery = useSuspenseQuery(
|
||||
artistsQueries.albumArtistDetail({
|
||||
query: { id: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const albumCount = detailQuery?.data?.albumCount;
|
||||
const songCount = detailQuery?.data?.songCount;
|
||||
const duration = detailQuery?.data?.duration;
|
||||
const albumCount = detailQuery.data?.albumCount;
|
||||
const songCount = detailQuery.data?.songCount;
|
||||
const duration = detailQuery.data?.duration;
|
||||
const durationEnabled = duration !== null && duration !== undefined;
|
||||
|
||||
const metadataItems = [
|
||||
@@ -66,62 +68,82 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
||||
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = (type?: Play) => {
|
||||
if (!server?.id || !routeId) return;
|
||||
addToQueueByFetch(
|
||||
server.id,
|
||||
[routeId],
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
type || playButtonBehavior,
|
||||
);
|
||||
};
|
||||
const handlePlay = useCallback(
|
||||
(type?: Play) => {
|
||||
if (!server?.id || !routeId) return;
|
||||
addToQueueByFetch(
|
||||
server.id,
|
||||
[routeId],
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
type || playButtonBehavior,
|
||||
);
|
||||
},
|
||||
[addToQueueByFetch, playButtonBehavior, routeId, server.id],
|
||||
);
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
const handleFavorite = useCallback(() => {
|
||||
if (!detailQuery.data) return;
|
||||
setFavorite(
|
||||
detailQuery.data._serverId,
|
||||
[detailQuery.data.id],
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
!detailQuery.data.userFavorite,
|
||||
);
|
||||
};
|
||||
}, [detailQuery.data, setFavorite]);
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!detailQuery?.data) return;
|
||||
const handleUpdateRating = useCallback(
|
||||
(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(
|
||||
detailQuery.data._serverId,
|
||||
[detailQuery.data.id],
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
0,
|
||||
rating,
|
||||
);
|
||||
}
|
||||
},
|
||||
[detailQuery.data, setRating],
|
||||
);
|
||||
|
||||
return setRating(
|
||||
detailQuery.data._serverId,
|
||||
[detailQuery.data.id],
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
rating,
|
||||
);
|
||||
};
|
||||
const handleMoreOptions = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!detailQuery.data) return;
|
||||
ContextMenuController.call({
|
||||
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
||||
event: e,
|
||||
});
|
||||
},
|
||||
[detailQuery.data],
|
||||
);
|
||||
|
||||
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!detailQuery?.data) return;
|
||||
ContextMenuController.call({
|
||||
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
||||
event: e,
|
||||
});
|
||||
};
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: detailQuery.data?.imageId || undefined,
|
||||
imageUrl: detailQuery.data?.imageUrl,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
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 (
|
||||
<LibraryHeader
|
||||
imageUrl={detailQuery?.data?.imageUrl}
|
||||
imageUrl={selectedImageUrl}
|
||||
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
|
||||
ref={ref}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
title={detailQuery.data?.name || ''}
|
||||
>
|
||||
<Stack gap="md" w="100%">
|
||||
<Group className={styles.metadataGroup}>
|
||||
@@ -135,13 +157,13 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
||||
))}
|
||||
</Group>
|
||||
<LibraryHeaderMenu
|
||||
favorite={detailQuery?.data?.userFavorite}
|
||||
favorite={detailQuery.data?.userFavorite}
|
||||
onFavorite={handleFavorite}
|
||||
onMore={handleMoreOptions}
|
||||
onPlay={(type) => handlePlay(type)}
|
||||
onRating={showRating ? handleUpdateRating : undefined}
|
||||
onShuffle={() => handlePlay(Play.SHUFFLE)}
|
||||
rating={detailQuery?.data?.userRating || 0}
|
||||
rating={detailQuery.data?.userRating || 0}
|
||||
/>
|
||||
</Stack>
|
||||
</LibraryHeader>
|
||||
|
||||
@@ -31,6 +31,7 @@ export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
|
||||
controls={controls}
|
||||
data={albumArtist}
|
||||
enableDrag
|
||||
isRound
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
|
||||
@@ -94,6 +94,7 @@ export const AlbumArtistListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -105,6 +106,7 @@ export const AlbumArtistListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useListContext } from '/@/renderer/context/list-context';
|
||||
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||
@@ -18,7 +19,6 @@ interface AlbumArtistListHeaderProps {
|
||||
export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { itemCount } = useListContext();
|
||||
const pageTitle = title || t('page.albumArtistList.title', { postProcess: 'titleCase' });
|
||||
|
||||
return (
|
||||
@@ -27,9 +27,7 @@ export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) =>
|
||||
<LibraryHeaderBar ignoreMaxWidth>
|
||||
<PlayButton />
|
||||
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
<AlbumArtistListHeaderBadge />
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<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 { query } = useAlbumArtistListFilters();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export const AlbumArtistListInfiniteGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: AlbumArtistListInfiniteGridProps) => {
|
||||
const listCountQuery = artistsQueries.albumArtistListCount({
|
||||
query: { ...query },
|
||||
@@ -65,6 +66,7 @@ export const AlbumArtistListInfiniteGrid = ({
|
||||
onRangeChanged={onRangeChanged}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ export const AlbumArtistListPaginatedGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: AlbumArtistListPaginatedGridProps) => {
|
||||
const listCountQuery = artistsQueries.albumArtistListCount({
|
||||
query: { ...query },
|
||||
@@ -77,6 +78,7 @@ export const AlbumArtistListPaginatedGrid = ({
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
|
||||
@@ -86,6 +86,7 @@ export const ArtistListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -97,6 +98,7 @@ export const ArtistListView = ({
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useListContext } from '/@/renderer/context/list-context';
|
||||
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
|
||||
@@ -18,7 +19,6 @@ interface ArtistListHeaderProps {
|
||||
export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { itemCount } = useListContext();
|
||||
const pageTitle = title || t('entity.artist_other', { postProcess: 'titleCase' });
|
||||
|
||||
return (
|
||||
@@ -27,9 +27,7 @@ export const ArtistListHeader = ({ title }: ArtistListHeaderProps) => {
|
||||
<LibraryHeaderBar ignoreMaxWidth>
|
||||
<PlayButton />
|
||||
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
<ArtistListHeaderBadge />
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<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 { query } = useArtistListFilters();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ArtistListInfiniteGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: ArtistListInfiniteGridProps) => {
|
||||
const listCountQuery = artistsQueries.artistListCount({
|
||||
query: { ...query },
|
||||
@@ -64,6 +65,7 @@ export const ArtistListInfiniteGrid = ({
|
||||
onRangeChanged={onRangeChanged}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ArtistListPaginatedGrid = ({
|
||||
},
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size,
|
||||
}: ArtistListPaginatedGridProps) => {
|
||||
const listCountQuery = artistsQueries.artistListCount({
|
||||
query: { ...query },
|
||||
@@ -76,6 +77,7 @@ export const ArtistListPaginatedGrid = ({
|
||||
itemType={LibraryItem.ARTIST}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
rows={rows}
|
||||
size={size}
|
||||
/>
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
import { Suspense, useRef } from 'react';
|
||||
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 { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||
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 { useFastAverageColor, useWaitForColorCalculation } from '/@/renderer/hooks';
|
||||
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const AlbumArtistDetailRoute = () => {
|
||||
const AlbumArtistDetailRouteContent = () => {
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const server = useCurrentServer();
|
||||
@@ -31,18 +33,29 @@ const AlbumArtistDetailRoute = () => {
|
||||
|
||||
const routeId = (artistId || albumArtistId) as string;
|
||||
|
||||
const location = useLocation();
|
||||
const detailQuery = useSuspenseQuery(
|
||||
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
||||
);
|
||||
|
||||
const detailQuery = useSuspenseQuery({
|
||||
...artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
||||
initialData: location.state?.item,
|
||||
staleTime: 0,
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: detailQuery.data?.imageId || undefined,
|
||||
imageUrl: detailQuery.data?.imageUrl,
|
||||
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({
|
||||
id: artistId,
|
||||
src: detailQuery.data?.imageUrl,
|
||||
srcLoaded: !detailQuery.isLoading,
|
||||
src: selectedImageUrl,
|
||||
srcLoaded: true,
|
||||
});
|
||||
|
||||
const background = backgroundColor;
|
||||
@@ -50,14 +63,14 @@ const AlbumArtistDetailRoute = () => {
|
||||
const showBlurredImage = artistBackground;
|
||||
|
||||
const { isReady } = useWaitForColorCalculation({
|
||||
hasImage: !!detailQuery.data?.imageUrl,
|
||||
hasImage: !!selectedImageUrl,
|
||||
isLoading: isColorLoading,
|
||||
routeId,
|
||||
showBlurredImage,
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
return <Spinner container />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -73,7 +86,7 @@ const AlbumArtistDetailRoute = () => {
|
||||
variant="default"
|
||||
/>
|
||||
<LibraryHeaderBar.Title>
|
||||
{detailQuery?.data?.name}
|
||||
{detailQuery.data?.name}
|
||||
</LibraryHeaderBar.Title>
|
||||
</LibraryHeaderBar>
|
||||
),
|
||||
@@ -86,7 +99,7 @@ const AlbumArtistDetailRoute = () => {
|
||||
<LibraryBackgroundImage
|
||||
blur={artistBackgroundBlur}
|
||||
headerRef={headerRef}
|
||||
imageUrl={detailQuery.data?.imageUrl || ''}
|
||||
imageUrl={libraryBackgroundImageUrl || ''}
|
||||
/>
|
||||
) : (
|
||||
<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 = () => {
|
||||
return (
|
||||
<PageErrorBoundary>
|
||||
|
||||
@@ -127,6 +127,7 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
itemType={LibraryItem.SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
|
||||
@@ -202,8 +202,12 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
||||
}
|
||||
|
||||
if (allSongIds.length === 0) {
|
||||
toast.warn({
|
||||
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
||||
toast.success({
|
||||
message: t('form.addToPlaylist.success', {
|
||||
message: 0,
|
||||
numOfPlaylists: 1,
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -241,8 +245,12 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
||||
}
|
||||
|
||||
if (songsToAdd.length === 0) {
|
||||
toast.warn({
|
||||
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
||||
toast.success({
|
||||
message: t('form.addToPlaylist.success', {
|
||||
message: 0,
|
||||
numOfPlaylists: 1,
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user