Compare commits

...

60 Commits

Author SHA1 Message Date
jeffvli 54b18601b8 Remove playlist detail route file 2023-12-19 14:59:32 -08:00
jeffvli 0cd0032966 Fix list sort 2023-12-19 14:59:15 -08:00
jeffvli d6cc6a4745 Support subsonic song filters 2023-12-19 14:58:52 -08:00
jeffvli f7fcf6c079 Support subsonic album filters 2023-12-18 12:02:41 -08:00
jeffvli 4051e9dfa3 Use imported jellyfin controller 2023-12-18 11:46:05 -08:00
jeffvli 5a94f70e63 Add list count endpoints to jf/nd 2023-12-18 11:45:04 -08:00
jeffvli 50dd70df81 Add global sort utils 2023-12-13 18:19:58 -08:00
jeffvli 8493668c97 Remove default playlist page 2023-12-13 18:19:58 -08:00
jeffvli d347221be5 Support playlists 2023-12-13 18:19:58 -08:00
jeffvli 18ec50b2a3 Support album and artist detail pages for subsonic 2023-12-13 18:19:58 -08:00
jeffvli 3c691d23d9 Return similar artists on artist detail 2023-12-13 18:19:57 -08:00
jeffvli 8ce2a99d37 Refactor sidebar playlist 2023-12-13 18:19:57 -08:00
jeffvli 567424011f Add subsonic in server entry form 2023-12-13 18:19:57 -08:00
jeffvli b2f14d7369 Support entity list pages for subsonic 2023-12-13 18:19:57 -08:00
jeffvli 2ecafea759 Fix album count translation string 2023-12-13 18:19:57 -08:00
jeffvli b7bbba928d Update log format 2023-12-13 18:19:57 -08:00
jeffvli 33b522a2f3 Fix expected controller responses 2023-12-13 18:19:57 -08:00
jeffvli f8d109fce4 Set search query to required 2023-12-13 18:19:57 -08:00
jeffvli 8fcf5291c4 Add first iteration of new subsonic controller 2023-12-13 18:19:57 -08:00
jeffvli 3b155cc6e8 Remove throw from log function
- Typescript cannot determine if a function throws an error
- Does not work as a type guard when using ts-rest
2023-12-13 18:19:57 -08:00
jeffvli 509627a0ad Allow null totalRecordCount on paginated response 2023-12-13 18:19:57 -08:00
jeffvli d08d3686de Add logger function 2023-12-13 18:19:57 -08:00
jeffvli ca695ca155 Add all relevant subsonic endpoints to ts-rest 2023-12-13 18:19:57 -08:00
jeffvli 7b639b45f7 Add new translations 2023-12-13 18:19:24 -08:00
Hosted Weblate 85d9162b12 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 36670b330f Translated using Weblate (Swedish)
Currently translated at 52.6% (273 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mattias <mattiasghodsian@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sv/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 9c380a8241 Translated using Weblate (French)
Currently translated at 99.4% (516 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate c26820ee82 Translated using Weblate (Dutch)
Currently translated at 35.8% (186 of 519 strings)

Translated using Weblate (Dutch)

Currently translated at 32.3% (168 of 519 strings)

Translated using Weblate (Dutch)

Currently translated at 15.2% (79 of 519 strings)

Added translation using Weblate (Dutch)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Idris Saklou <idrissaklou@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate dccd6afc3d Translated using Weblate (Italian)
Currently translated at 99.0% (514 of 519 strings)

Translated using Weblate (Italian)

Currently translated at 96.5% (501 of 519 strings)

Co-authored-by: Aurora <arci@anche.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NicKoehler <grillinicola@proton.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate c6a520b0d7 Translated using Weblate (Polish)
Currently translated at 99.8% (518 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 1f4f3a5497 Translated using Weblate (Czech)
Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 58d04b3126 Translated using Weblate (German)
Currently translated at 88.8% (461 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Maik <maikguenes2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate fcac4a5547 Translated using Weblate (Portuguese (Brazil))
Currently translated at 28.3% (147 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Kendall Garner c05b474827 fix navi null date (#408) 2023-12-13 00:28:53 -08:00
mcneb10 a8814d3e8a Fix 'undefined' in window title when song has no artist name (#402) 2023-12-05 19:05:08 -08:00
Kendall Garner 3f9cdab450 convert value to number on set (#390) 2023-12-04 20:20:19 -08:00
jeffvli 1d2e9484d8 Bump node-abi version 2023-11-18 01:49:00 -08:00
jeffvli f5ec294e0c Add new languages 2023-11-18 01:32:59 -08:00
jeffvli 0beef2a0b7 Bump electron builder version 2023-11-18 01:30:19 -08:00
Hosted Weblate 86209b6272 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (518 of 518 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kaiyang Wu <self@origincode.me>
Co-authored-by: kare-Udon <laoliu735@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate b32afc0e49 Translated using Weblate (Serbian)
Currently translated at 100.0% (518 of 518 strings)

Added translation using Weblate (Serbian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ilija <zojka2g@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sr/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate 58c4ab4a67 Translated using Weblate (Swedish)
Currently translated at 43.4% (225 of 518 strings)

Added translation using Weblate (Swedish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mattias <mattiasghodsian@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sv/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate bcbd169507 Translated using Weblate (French)
Currently translated at 94.9% (492 of 518 strings)

Translated using Weblate (French)

Currently translated at 94.2% (488 of 518 strings)

Translated using Weblate (French)

Currently translated at 94.2% (488 of 518 strings)

Translated using Weblate (French)

Currently translated at 90.3% (468 of 518 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate 55a4e74118 Translated using Weblate (Spanish)
Currently translated at 100.0% (518 of 518 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate e4c449d6de Translated using Weblate (Italian)
Currently translated at 96.1% (498 of 518 strings)

Translated using Weblate (Italian)

Currently translated at 96.1% (498 of 518 strings)

Co-authored-by: Aurora <arci@anche.no>
Co-authored-by: CraftWorks <thelonegamer87@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate aa5004c866 Translated using Weblate (Polish)
Currently translated at 98.6% (511 of 518 strings)

Translated using Weblate (Polish)

Currently translated at 98.6% (511 of 518 strings)

Co-authored-by: 7Adrian <7adrian.mail@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate a1aa5f323c Translated using Weblate (Czech)
Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (518 of 518 strings)

Added translation using Weblate (Czech)

Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate 92f91be650 Translated using Weblate (Japanese)
Currently translated at 100.0% (518 of 518 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: aorinngoDo <aorinngo@email.cz>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ja/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate 419462b22b Translated using Weblate (Russian)
Currently translated at 72.2% (374 of 518 strings)

Translated using Weblate (Russian)

Currently translated at 72.0% (373 of 518 strings)

Translated using Weblate (Russian)

Currently translated at 72.0% (373 of 518 strings)

Co-authored-by: Arseniy <_senyaa@tutanota.com>
Co-authored-by: Gitized <s.v.lazarev.89@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate 22e31a7b09 Translated using Weblate (German)
Currently translated at 87.0% (451 of 518 strings)

Translated using Weblate (German)

Currently translated at 87.0% (451 of 518 strings)

Translated using Weblate (German)

Currently translated at 62.7% (325 of 518 strings)

Translated using Weblate (German)

Currently translated at 62.7% (325 of 518 strings)

Translated using Weblate (German)

Currently translated at 59.6% (309 of 518 strings)

Translated using Weblate (German)

Currently translated at 59.6% (309 of 518 strings)

Translated using Weblate (German)

Currently translated at 56.9% (295 of 518 strings)

Translated using Weblate (German)

Currently translated at 56.9% (295 of 518 strings)

Translated using Weblate (German)

Currently translated at 55.4% (287 of 518 strings)

Translated using Weblate (German)

Currently translated at 55.4% (287 of 518 strings)

Translated using Weblate (German)

Currently translated at 53.8% (279 of 518 strings)

Translated using Weblate (German)

Currently translated at 53.8% (279 of 518 strings)

Translated using Weblate (German)

Currently translated at 1.9% (10 of 518 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobayashi <kobayashi90@protonmail.ch>
Co-authored-by: Rudi Mentaire <stoertebecker@byom.de>
Co-authored-by: ThetaDev <t.testboy@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
Hosted Weblate dce0284e0b Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.5% (122 of 518 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 22.5% (117 of 518 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 13.3% (69 of 518 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2023-11-18 10:18:08 +01:00
jeffvli 2afe7e8920 Bump to v0.5.2 2023-11-18 01:17:57 -08:00
jeffvli 9ca364dd0e Fix title case transformer 2023-11-17 02:03:01 -08:00
jeffvli ccd8d2b6b0 Add network error catch 2023-11-16 23:35:26 -08:00
jeffvli fdfbad68e2 Fix app single instance lock (#385) 2023-11-16 23:29:19 -08:00
jeffvli 48a529dd51 Bump electron to v27.1.0 (#383) 2023-11-16 10:17:39 -08:00
jeffvli bc40f93b59 Fix translation titlecase for accented characters (#357) 2023-11-12 03:46:12 -08:00
doggo cf544bea61 Fixed incorrect docker command argument (#365) 2023-11-12 03:40:38 -08:00
Samuli Piipponen f24cf5a928 Fix Discord status with no Artists (#359) 2023-11-12 03:40:28 -08:00
Kendall Garner 11af31c539 [bugfix]: correct text for albumDetail (#376) 2023-11-12 03:40:17 -08:00
79 changed files with 7767 additions and 1997 deletions
+2 -2
View File
@@ -59,11 +59,11 @@ Feishin is also available as a Docker image. The images are hosted via `ghcr.io`
```bash
# Run the latest version
docker run --name feishin --port 9180:9180 ghcr.io/jeffvli/feishin:latest
docker run --name feishin -p 9180:9180 ghcr.io/jeffvli/feishin:latest
# Build the image locally
docker build -t feishin .
docker run --name feishin --port 9180:9180 feishin
docker run --name feishin -p 9180:9180 feishin
```
### Configuration
+230 -230
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.5.1",
"version": "0.5.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.5.1",
"version": "0.5.2",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
@@ -103,8 +103,8 @@
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"detect-port": "^1.3.0",
"electron": "^25.8.1",
"electron-builder": "^24.6.3",
"electron": "^27.1.0",
"electron-builder": "^24.9.0",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
"electronmon": "^2.0.2",
@@ -2358,12 +2358,11 @@
}
},
"node_modules/@electron/asar": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"dev": true,
"dependencies": {
"chromium-pickle-js": "^0.2.0",
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
@@ -2406,13 +2405,14 @@
}
},
"node_modules/@electron/notarize": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz",
"integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz",
"integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"fs-extra": "^9.0.1"
"fs-extra": "^9.0.1",
"promise-retry": "^2.0.1"
},
"engines": {
"node": ">= 10.0.0"
@@ -2446,18 +2446,18 @@
}
},
"node_modules/@electron/notarize/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@electron/osx-sign": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.4.tgz",
"integrity": "sha512-xfhdEcIOfAZg7scZ9RQPya1G1lWo8/zMCwUXAulq0SfY7ONIW+b9qGyKdMyuMctNYwllrIS+vmxfijSfjeh97g==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz",
"integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==",
"dev": true,
"dependencies": {
"compare-version": "^0.1.2",
@@ -2514,9 +2514,9 @@
}
},
"node_modules/@electron/osx-sign/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -2907,9 +2907,9 @@
}
},
"node_modules/@electron/universal": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.3.4.tgz",
"integrity": "sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz",
"integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==",
"dev": true,
"dependencies": {
"@electron/asar": "^3.2.1",
@@ -2952,9 +2952,9 @@
}
},
"node_modules/@electron/universal/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -3736,9 +3736,9 @@
}
},
"node_modules/@malept/flatpak-bundler/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -4779,9 +4779,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
@@ -4980,9 +4980,9 @@
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"node_modules/@types/node": {
@@ -5003,9 +5003,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/plist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz",
"integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
"integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
"optional": true,
"dependencies": {
@@ -5196,9 +5196,9 @@
}
},
"node_modules/@types/verror": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
"integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==",
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
"integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==",
"dev": true,
"optional": true
},
@@ -5668,9 +5668,9 @@
"dev": true
},
"node_modules/7zip-bin": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
"node_modules/abab": {
@@ -5951,26 +5951,26 @@
"dev": true
},
"node_modules/app-builder-lib": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.3.tgz",
"integrity": "sha512-++0Zp7vcCHfXMBGVj7luFxpqvMPk5mcWeTuw7OK0xNAaNtYQTTN0d9YfWRsb1MvviTOOhyHeULWz1CaixrdrDg==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.0.tgz",
"integrity": "sha512-eqxC5QZQoZzwqBkd9Rd0O3T/VaSOmgW9pgNc+tXrEktpQ56cEFt4s1AaQjGrLSajamXerVj6bZM5yZFp+CCyqA==",
"dev": true,
"dependencies": {
"@develar/schema-utils": "~2.6.5",
"@electron/notarize": "^1.2.3",
"@electron/osx-sign": "^1.0.4",
"@electron/universal": "1.3.4",
"@electron/notarize": "2.1.0",
"@electron/osx-sign": "1.0.5",
"@electron/universal": "1.4.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"electron-publish": "24.5.0",
"electron-publish": "24.8.1",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -5999,9 +5999,9 @@
}
},
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -6050,9 +6050,9 @@
}
},
"node_modules/app-builder-lib/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -6267,9 +6267,9 @@
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"dev": true
},
"node_modules/async-exit-hook": {
@@ -6875,16 +6875,16 @@
"dev": true
},
"node_modules/builder-util": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
"dev": true,
"dependencies": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -6911,9 +6911,9 @@
}
},
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -6950,9 +6950,9 @@
}
},
"node_modules/builder-util/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -8629,14 +8629,14 @@
}
},
"node_modules/dmg-builder": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.3.tgz",
"integrity": "sha512-O7KNT7OKqtV54fMYUpdlyTOCP5DoPuRMLqMTgxxV2PO8Hj/so6zOl5o8GTs8pdDkeAhJzCFOUNB3BDhgXbUbJg==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.0.tgz",
"integrity": "sha512-0fQdxPtYQYyjj2BScYGhjG6KHp7kcL4+5+X1Kug3zD7IIS7ROv2PV2H3HgGSh9NtUYeY9FLLPKSfggrzj5ZC4Q==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.0",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
@@ -8646,9 +8646,9 @@
}
},
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -8685,9 +8685,9 @@
}
},
"node_modules/dmg-builder/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -8920,9 +8920,9 @@
}
},
"node_modules/electron": {
"version": "25.8.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz",
"integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==",
"version": "27.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-27.1.0.tgz",
"integrity": "sha512-XPdJiO475QJ8cx59/goWNNWnlV0vab+Ut3occymos7VDxkHV5mFrlW6tcGi+M3bW6gBfwpJocWMng8tw542vww==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -8938,16 +8938,16 @@
}
},
"node_modules/electron-builder": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.3.tgz",
"integrity": "sha512-O6PqhRXwfxCNTXI4BlhELSeYYO6/tqlxRuy+4+xKBokQvwDDjDgZMMoSgAmanVSCuzjE7MZldI9XYrKFk+EQDw==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.0.tgz",
"integrity": "sha512-jA+jYCZlwYzeJEkb82eZNbdMVTUIh99+JQL3yCZjeV3J1N+pdpDrS0P8wZX8vOGpR310TU0tqgjpkxVgE+38tg==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.0",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"dmg-builder": "24.6.3",
"dmg-builder": "24.9.0",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
@@ -8964,9 +8964,9 @@
}
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -9153,14 +9153,14 @@
}
},
"node_modules/electron-publish": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
"dev": true,
"dependencies": {
"@types/fs-extra": "^9.0.11",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
@@ -9168,9 +9168,9 @@
}
},
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -9207,9 +9207,9 @@
}
},
"node_modules/electron-publish/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -15142,9 +15142,9 @@
}
},
"node_modules/node-abi": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz",
"integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==",
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
"integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==",
"dev": true,
"dependencies": {
"semver": "^7.3.5"
@@ -19439,9 +19439,9 @@
}
},
"node_modules/temp-file/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -22887,12 +22887,11 @@
"dev": true
},
"@electron/asar": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"dev": true,
"requires": {
"chromium-pickle-js": "^0.2.0",
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
@@ -22923,13 +22922,14 @@
}
},
"@electron/notarize": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz",
"integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz",
"integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"fs-extra": "^9.0.1"
"fs-extra": "^9.0.1",
"promise-retry": "^2.0.1"
},
"dependencies": {
"fs-extra": {
@@ -22955,17 +22955,17 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
},
"@electron/osx-sign": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.4.tgz",
"integrity": "sha512-xfhdEcIOfAZg7scZ9RQPya1G1lWo8/zMCwUXAulq0SfY7ONIW+b9qGyKdMyuMctNYwllrIS+vmxfijSfjeh97g==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz",
"integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==",
"dev": true,
"requires": {
"compare-version": "^0.1.2",
@@ -23004,9 +23004,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -23301,9 +23301,9 @@
}
},
"@electron/universal": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.3.4.tgz",
"integrity": "sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz",
"integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==",
"dev": true,
"requires": {
"@electron/asar": "^3.2.1",
@@ -23338,9 +23338,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -23967,9 +23967,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -24734,9 +24734,9 @@
}
},
"@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"requires": {
"@types/ms": "*"
@@ -24935,9 +24935,9 @@
"dev": true
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"@types/node": {
@@ -24958,9 +24958,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/plist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz",
"integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
"integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
"optional": true,
"requires": {
@@ -25150,9 +25150,9 @@
}
},
"@types/verror": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
"integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==",
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
"integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==",
"dev": true,
"optional": true
},
@@ -25502,9 +25502,9 @@
"dev": true
},
"7zip-bin": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
"abab": {
@@ -25709,26 +25709,26 @@
"dev": true
},
"app-builder-lib": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.3.tgz",
"integrity": "sha512-++0Zp7vcCHfXMBGVj7luFxpqvMPk5mcWeTuw7OK0xNAaNtYQTTN0d9YfWRsb1MvviTOOhyHeULWz1CaixrdrDg==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.0.tgz",
"integrity": "sha512-eqxC5QZQoZzwqBkd9Rd0O3T/VaSOmgW9pgNc+tXrEktpQ56cEFt4s1AaQjGrLSajamXerVj6bZM5yZFp+CCyqA==",
"dev": true,
"requires": {
"@develar/schema-utils": "~2.6.5",
"@electron/notarize": "^1.2.3",
"@electron/osx-sign": "^1.0.4",
"@electron/universal": "1.3.4",
"@electron/notarize": "2.1.0",
"@electron/osx-sign": "1.0.5",
"@electron/universal": "1.4.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"electron-publish": "24.5.0",
"electron-publish": "24.8.1",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -25754,9 +25754,9 @@
}
},
"builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@@ -25794,9 +25794,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -25961,9 +25961,9 @@
"dev": true
},
"async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"dev": true
},
"async-exit-hook": {
@@ -26424,16 +26424,16 @@
"dev": true
},
"builder-util": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
"dev": true,
"requires": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -26448,9 +26448,9 @@
},
"dependencies": {
"builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@@ -26479,9 +26479,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -27724,14 +27724,14 @@
}
},
"dmg-builder": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.3.tgz",
"integrity": "sha512-O7KNT7OKqtV54fMYUpdlyTOCP5DoPuRMLqMTgxxV2PO8Hj/so6zOl5o8GTs8pdDkeAhJzCFOUNB3BDhgXbUbJg==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.0.tgz",
"integrity": "sha512-0fQdxPtYQYyjj2BScYGhjG6KHp7kcL4+5+X1Kug3zD7IIS7ROv2PV2H3HgGSh9NtUYeY9FLLPKSfggrzj5ZC4Q==",
"dev": true,
"requires": {
"app-builder-lib": "24.6.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.0",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"dmg-license": "^1.0.11",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
@@ -27739,9 +27739,9 @@
},
"dependencies": {
"builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@@ -27770,9 +27770,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -27958,9 +27958,9 @@
}
},
"electron": {
"version": "25.8.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz",
"integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==",
"version": "27.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-27.1.0.tgz",
"integrity": "sha512-XPdJiO475QJ8cx59/goWNNWnlV0vab+Ut3occymos7VDxkHV5mFrlW6tcGi+M3bW6gBfwpJocWMng8tw542vww==",
"dev": true,
"requires": {
"@electron/get": "^2.0.0",
@@ -27977,16 +27977,16 @@
}
},
"electron-builder": {
"version": "24.6.3",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.3.tgz",
"integrity": "sha512-O6PqhRXwfxCNTXI4BlhELSeYYO6/tqlxRuy+4+xKBokQvwDDjDgZMMoSgAmanVSCuzjE7MZldI9XYrKFk+EQDw==",
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.0.tgz",
"integrity": "sha512-jA+jYCZlwYzeJEkb82eZNbdMVTUIh99+JQL3yCZjeV3J1N+pdpDrS0P8wZX8vOGpR310TU0tqgjpkxVgE+38tg==",
"dev": true,
"requires": {
"app-builder-lib": "24.6.3",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.0",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"dmg-builder": "24.6.3",
"dmg-builder": "24.9.0",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
@@ -27996,9 +27996,9 @@
},
"dependencies": {
"builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@@ -28154,14 +28154,14 @@
}
},
"electron-publish": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
"dev": true,
"requires": {
"@types/fs-extra": "^9.0.11",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
@@ -28169,9 +28169,9 @@
},
"dependencies": {
"builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"requires": {
"debug": "^4.3.4",
@@ -28200,9 +28200,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
@@ -32647,9 +32647,9 @@
}
},
"node-abi": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz",
"integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==",
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
"integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==",
"dev": true,
"requires": {
"semver": "^7.3.5"
@@ -35821,9 +35821,9 @@
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
}
}
+4 -4
View File
@@ -2,7 +2,7 @@
"name": "feishin",
"productName": "Feishin",
"description": "Feishin music server",
"version": "0.5.1",
"version": "0.5.2",
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
@@ -56,7 +56,7 @@
"package.json"
],
"afterSign": ".erb/scripts/notarize.js",
"electronVersion": "25.8.1",
"electronVersion": "27.1.0",
"mac": {
"target": {
"target": "default",
@@ -230,8 +230,8 @@
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"detect-port": "^1.3.0",
"electron": "^25.8.1",
"electron-builder": "^24.6.3",
"electron": "^27.1.0",
"electron-builder": "^24.9.0",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
"electronmon": "^2.0.2",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.5.1",
"version": "0.5.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.5.1",
"version": "0.5.2",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.5.1",
"version": "0.5.2",
"description": "",
"main": "./dist/main/main.js",
"author": {
+38 -5
View File
@@ -11,6 +11,11 @@ import de from './locales/de.json';
import it from './locales/it.json';
import ru from './locales/ru.json';
import ptBr from './locales/pt-BR.json';
import sr from './locales/sr.json';
import sv from './locales/sv.json';
import cs from './locales/cs.json';
import nbNO from './locales/nb-NO.json';
import nl from './locales/nl.json';
const resources = {
en: { translation: en },
@@ -23,6 +28,11 @@ const resources = {
ja: { translation: ja },
pl: { translation: pl },
'zh-Hans': { translation: zhHans },
sr: { translation: sr },
sv: { translation: sv },
cs: { translation: cs },
nl: { translation: nl },
'nb-NO': { translation: nbNO },
};
export const languages = [
@@ -30,6 +40,10 @@ export const languages = [
label: 'English',
value: 'en',
},
{
label: 'Čeština',
value: 'cs',
},
{
label: 'Español',
value: 'es',
@@ -51,9 +65,14 @@ export const languages = [
value: 'ja',
},
{
label: 'Русский',
value: 'ru',
label: 'Nederlands',
value: 'nl',
},
{
label: 'Norsk (Bokmål)',
value: 'nb-NO',
},
{
label: 'Português (Brasil)',
value: 'pt-BR',
@@ -62,6 +81,18 @@ export const languages = [
label: 'Polski',
value: 'pl',
},
{
label: 'Русский',
value: 'ru',
},
{
label: 'Srpski',
value: 'sr',
},
{
label: 'Svenska',
value: 'sv',
},
{
label: '简体中文',
value: 'zh-Hans',
@@ -88,8 +119,8 @@ const titleCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'titleCase',
process: (value: string) => {
return value.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
return value.replace(/\S\S*/g, (txt) => {
return txt.charAt(0).toLocaleUpperCase() + txt.slice(1).toLowerCase();
});
},
};
@@ -102,7 +133,9 @@ const sentenceCasePostProcessor: PostProcessorModule = {
return sentences
.map((sentence) => {
return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLocaleLowerCase();
return (
sentence.charAt(0).toLocaleUpperCase() + sentence.slice(1).toLocaleLowerCase()
);
})
.join('. ');
},
+632
View File
@@ -0,0 +1,632 @@
{
"player": {
"repeat_all": "opakovat vše",
"stop": "zastavit",
"repeat": "opakovat",
"queue_remove": "odebrat vybrané",
"playRandom": "přehrát náhodné",
"skip": "přeskočit",
"previous": "předchozí",
"toggleFullscreenPlayer": "přepnout celoobrazovkový přehrávač",
"skip_back": "přeskočit dozadu",
"favorite": "oblíbené",
"next": "další",
"shuffle": "náhodně",
"playbackFetchNoResults": "nenalezeny žádné skladby",
"playbackFetchInProgress": "načítání skladeb…",
"addNext": "přidat další",
"playbackSpeed": "rychlost přehrávání",
"playbackFetchCancel": "chvíli to trvá… zavřete oznámení pro zrušení akce",
"play": "přehrát",
"repeat_off": "opakování zakázáno",
"pause": "pozastavit",
"queue_clear": "vymazat frontu",
"muted": "ztlumeno",
"unfavorite": "odebrat z oblíbených",
"queue_moveToTop": "přesunout vybrané dolů",
"queue_moveToBottom": "přesunout vybrané nahoru",
"shuffle_off": "náhodně zakázáno",
"addLast": "přidat poslední",
"mute": "ztlumit",
"skip_forward": "přeskočit dopředu"
},
"setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
"remotePort_description": "nastavení portu pro server vzdáleného ovládání",
"hotkey_skipBackward": "přeskočení zpět",
"replayGainMode_description": "úprava zesílení hlasitosti podle hodnot {{ReplayGain}} uložených v metadatech souborů",
"volumeWheelStep_description": "počet procent, o které má být hlasitost posunuta při přejetí kolečkem myši na posuvníku hlasitosti",
"audioDevice_description": "vyberte zvukové zařízení k přehrávání (pouze webový přehrávač)",
"theme_description": "nastavení motivu použitého v aplikaci",
"hotkey_playbackPause": "pozastavení",
"replayGainFallback": "fallback {{ReplayGain}}",
"sidebarCollapsedNavigation_description": "zobrazit nebo skrýt navigaci ve sbaleném postranním panelu",
"mpvExecutablePath_help": "jedna na řádek",
"hotkey_volumeUp": "zvýšení hlasitosti",
"skipDuration": "doba k přeskočení",
"discordIdleStatus_description": "při povolení bude upraven stav když je přehrávač nečinný",
"showSkipButtons": "zobrazit tlačítka k přeskočení",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "minimální doba pro scrobblování (v procentech)",
"lyricFetch": "načtení textů z internetu",
"scrobble": "scrobblování",
"skipDuration_description": "nastavení doby k přeskočení při použití tlačítek k přeskočení na liště přehrávače",
"enableRemote_description": "povolí vzdálený ovládací server pro umožnění ostatním zařízením ovládat aplikaci",
"fontType_optionSystem": "systémové písmo",
"mpvExecutablePath_description": "nastavení cesty ke spustitelnému souboru mpv",
"replayGainClipping_description": "Zabránění clippingu způsobenému funkcí {{ReplayGain}} automatickým snížením zesílení",
"replayGainPreamp": "před-zesílení {{ReplayGain}} (dB)",
"hotkey_favoriteCurrentSong": "oblíbit $t(common.currentSong)",
"sampleRate": "vzorkovací frekvence",
"crossfadeStyle": "způsob prolnutí",
"sidePlayQueueStyle_optionAttached": "připojené",
"sidebarConfiguration": "nastavení postranního panelu",
"sampleRate_description": "vyberte výstupní vzorkovací frekvenci k použití, když je vybraná vzorkovací frekvence jiná, než ta u aktuálního média",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "clipping {{ReplayGain}}",
"hotkey_zoomIn": "přiblížení",
"scrobble_description": "scrobblovat přehrání na váš multimediální server",
"hotkey_browserForward": "vpřed v prohlížeči",
"audioExclusiveMode_description": "zapnout režim výhradního výstupu. V tomto režimu bude obvykle v systému schopný přehrávat zvuk pouze přehrávač mpv",
"discordUpdateInterval": "interval aktualizací {{discord}} rich presence",
"themeLight": "motiv (světlý)",
"fontType_optionBuiltIn": "vestavěné písmo",
"hotkey_playbackPlayPause": "přehrání / pozastavení",
"hotkey_rate1": "hodnocení 1 hvězdou",
"hotkey_skipForward": "přeskočení vpřed",
"disableLibraryUpdateOnStartup": "vypnout kontrolu nových verzí při spuštění",
"discordApplicationId_description": "id aplikace pro {{discord}} rich presence (výchozí je {{defaultId}})",
"sidePlayQueueStyle": "styl postranní fronty přehrávání",
"gaplessAudio": "zvuk bez mezer",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"zoom": "procento přiblížení",
"minimizeToTray_description": "minimalizovat aplikaci do systémové lišty",
"hotkey_playbackPlay": "přehrání",
"hotkey_togglePreviousSongFavorite": "přepnutí oblíbení u $t(common.previousSong)",
"hotkey_volumeDown": "snížení hlasitosti",
"hotkey_unfavoritePreviousSong": "zrušení oblíbení u $t(common.previousSong)",
"audioPlayer_description": "vyberte zvukový přehrávač pro použití k přehrávání",
"globalMediaHotkeys": "globální klávesové zkratky médií",
"hotkey_globalSearch": "globální vyhledávání",
"gaplessAudio_description": "nastavení přehrávače mpv pro přehrávání bez mezer",
"remoteUsername_description": "nastavení uživatelského jména pro server vzdáleného ovládání. pokud je jméno i heslo prázdné, bude autentifikace zakázána",
"disableAutomaticUpdates": "vypnout automatické aktualizace",
"exitToTray_description": "ukončit aplikaci do systémové lišty",
"followLyric_description": "přesouvat texty s aktuální pozicí přehrávání",
"hotkey_favoritePreviousSong": "oblíbit $t(common.previousSong)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"lyricOffset": "odsazení textů (ms)",
"discordUpdateInterval_description": "čas v sekundách mezi každou aktualizací (minimálně 15 sekund)",
"fontType_optionCustom": "vlastní písmo",
"themeDark_description": "nastavit použití tmavého motivu v aplikaci",
"audioExclusiveMode": "režim výhradního výstupu",
"remotePassword": "heslo serveru pro vzdálené ovládání",
"lyricFetchProvider": "poskytovatelé textů",
"language_description": "nastavení jazyka aplikace ($t(common.restartRequired))",
"playbackStyle_optionCrossFade": "křížové prolnutí",
"hotkey_rate3": "hodnocení 3 hvězdami",
"font": "písmo",
"mpvExtraParameters": "parametry mpv",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "nastavit použití světlého motivu v aplikaci",
"hotkey_toggleFullScreenPlayer": "přepnutí přehrávače na celou obrazovku",
"hotkey_localSearch": "vyhledávání na stránce",
"hotkey_toggleQueue": "přepnutí fronty",
"zoom_description": "nastavte procento přiblížení aplikace",
"remotePassword_description": "nastavení hesla pro server vzdáleného ovládání. Tyto údaje jsou ve výchozím nastavení přenášeny nezabezpečeným spojením, takže doporučujeme použití unikátního hesla, na kterém vám nezáleží",
"hotkey_rate5": "hodnocení 5 hvězdami",
"hotkey_playbackPrevious": "předchozí skladba",
"showSkipButtons_description": "zobrazit nebo skrýt tlačítka k přeskočení na liště přehrávače",
"crossfadeDuration_description": "nastavte trvání efektu prolnutí",
"language": "jazyk",
"playbackStyle": "způsob přehrávání",
"hotkey_toggleShuffle": "přepnutí náhodného přehrávání",
"theme": "motiv",
"playbackStyle_description": "nastavení způsobu přehrávání pro přehrávač zvuku",
"discordRichPresence_description": "povolit stav přehrávání v {{discord}} rich presence. Klíče obrázků jsou: {{icon}}, {{playing}}, {{paused}} ",
"mpvExecutablePath": "cesta ke spustitelnému souboru mpv",
"audioDevice": "zvukové zařízení",
"hotkey_rate2": "hodnocení 2 hvězdami",
"playButtonBehavior_description": "nastavení výchozího chování tlačítka přehrávání při přidávání skladeb do fronty",
"minimumScrobblePercentage_description": "minimální procento skladby, které musí být přehráno před jejím scrobblováním",
"exitToTray": "ukončit do lišty",
"hotkey_rate4": "hodnocení 4 hvězdami",
"enableRemote": "povolit vzdálený ovládací server",
"showSkipButton_description": "zobrazit nebo skrýt tlačítka k přeskočení na liště přehrávače",
"savePlayQueue": "uložit frontu přehrávání",
"minimumScrobbleSeconds_description": "minimální doba v sekundách, která musí být přehrána před scrobblováním skladby",
"skipPlaylistPage_description": "při navigaci na playlist přejít na stránku seznamu skladeb v playlistu namísto výchozí stránky",
"fontType_description": "vestavěné písmo vybere jedno z písem poskytovaných programem Feishin. systémové písmo vám umožní vybrat si jakékoli písmo poskytované vaším operačním systémem. vlastní vám umožňuje použít vaše vlastní písmo",
"playButtonBehavior": "chování tlačítka přehrávání",
"volumeWheelStep": "krok kolečka hlasitosti",
"sidebarPlaylistList_description": "zobrazit nebo skrýt seznam playlistů v postranním panelu",
"accentColor": "barva",
"sidePlayQueueStyle_description": "nastavení stylu postranní fronty přehrávání",
"accentColor_description": "nastaví barvu aplikace",
"replayGainMode": "režim {{ReplayGain}}",
"playbackStyle_optionNormal": "normální",
"windowBarStyle": "styl záhlaví okna",
"floatingQueueArea": "zobrazit plovoucí oblast přejetí nad frontou",
"replayGainFallback_description": "zesílení v db k použití, když nemá soubor žádné značky {{ReplayGain}}",
"replayGainPreamp_description": "úprava předběžného zesílení použitého na hodnoty {{ReplayGain}}",
"hotkey_toggleRepeat": "přepnutí opakování",
"lyricOffset_description": "odsazení textů o určité množství milisekund",
"sidebarConfiguration_description": "vyberte položky a pořadí, ve kterém budou v postranním panelu",
"fontType": "typ písma",
"remotePort": "port serveru vzdáleného ovládání",
"applicationHotkeys": "aplikační zkratky",
"hotkey_playbackNext": "další skladba",
"useSystemTheme_description": "následovat systémovou předvolbu světlého nebo tmavého motivu",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "načtení textů z různých internetových zdrojů",
"lyricFetchProvider_description": "vyberte poskytovatele textů. pořadí poskytovatelů je pořadí, ve kterém budou načítány",
"globalMediaHotkeys_description": "zapnout nebo vypnout použití vašich systémových zkratek médií pro ovládání přehrávače",
"customFontPath": "vlastní cesta k písmům",
"followLyric": "zobrazit aktuální texty",
"crossfadeDuration": "trvání prolnutí",
"discordIdleStatus": "zobrazit stav nečinnosti v rich presence",
"sidePlayQueueStyle_optionDetached": "odpojené",
"audioPlayer": "zvukový přehrávač",
"hotkey_zoomOut": "oddálení",
"hotkey_unfavoriteCurrentSong": "zrušení oblíbení u $t(common.currentSong)",
"hotkey_rate0": "vymazání hodnocení",
"discordApplicationId": "aplikační id pro {{discord}}",
"applicationHotkeys_description": "nastavení klávesových zkratek aplikace. přepněte pole pro nastavení jako globální zkratku (pouze na počítači)",
"floatingQueueArea_description": "zobrazit ikonu přejetí myší na pravé straně obrazovky pro zobrazení fronty",
"hotkey_volumeMute": "ztlumení",
"hotkey_toggleCurrentSongFavorite": "přepnutí oblíbení u $t(common.currentSong)",
"remoteUsername": "uživatelské jméno serveru vzdáleného ovládání",
"hotkey_browserBack": "zpět v prohlížeči",
"showSkipButton": "zobrazit tlačítka k přeskočení",
"sidebarPlaylistList": "postranní seznam playlistů",
"minimizeToTray": "minimalizovat do lišty",
"skipPlaylistPage": "přeskočit stránku playlistu",
"themeDark": "motiv (tmavý)",
"sidebarCollapsedNavigation": "postranní (sbalená) navigace",
"customFontPath_description": "nastavení cesty k vlastnímu písmu k využití v aplikaci",
"gaplessAudio_optionWeak": "slabý (doporučeno)",
"minimumScrobbleSeconds": "minimální scrobblování (v sekundách)",
"hotkey_playbackStop": "zastavení",
"windowBarStyle_description": "vyberte styl záhlaví okna",
"discordRichPresence": "{{discord}} rich presence",
"font_description": "nastavení písma použitého v aplikaci",
"savePlayQueue_description": "uložit frontu přehrávání, když je aplikace zavřena a obnovit ji při otevření aplikace",
"useSystemTheme": "použít systémový motiv"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist_one)",
"goToPage": "přejít na stránku",
"moveToTop": "přesunout nahoru",
"clearQueue": "vymazat frontu",
"addToFavorites": "přidat do $t(entity.favorite_other)",
"addToPlaylist": "přidat do $t(entity.playlist_one)",
"createPlaylist": "vytvořit $t(entity.playlist_one)",
"removeFromPlaylist": "odebrat z $t(entity.playlist_one)",
"viewPlaylists": "zobrazit $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "odstranit $t(entity.playlist_one)",
"removeFromQueue": "odebrat z fronty",
"deselectAll": "zrušit výběr všeho",
"moveToBottom": "přesunout dolů",
"setRating": "nastavit hodnocení",
"toggleSmartPlaylistEditor": "přepnout editor $t(entity.smartPlaylist)",
"removeFromFavorites": "odebrat z $t(entity.favorite_other)"
},
"common": {
"backward": "zpátky",
"increase": "zvýčit",
"rating": "hodnocení",
"bpm": "bpm",
"refresh": "obnovit",
"unknown": "neznámý",
"areYouSure": "opravdu?",
"edit": "upravit",
"favorite": "oblíbený",
"left": "vlevo",
"save": "uložit",
"right": "vpravo",
"currentSong": "aktuální $t(entity.track_one)",
"collapse": "sbalit",
"trackNumber": "stopa",
"descending": "sestupně",
"add": "přidat",
"gap": "mezera",
"ascending": "vzestupně",
"dismiss": "zavřít",
"year": "rok",
"manage": "správa",
"limit": "limit",
"minimize": "minimalizovat",
"modified": "upraveno",
"duration": "trvání",
"name": "název",
"maximize": "maximalizovat",
"decrease": "snížit",
"ok": "ok",
"description": "popis",
"configure": "nastavit",
"path": "cesta",
"center": "uprostřed",
"no": "ne",
"owner": "majitel",
"enable": "zapnout",
"clear": "vymazat",
"forward": "vpřed",
"delete": "odstranit",
"cancel": "zrušit",
"forceRestartRequired": "restartujte pro použití změn… zavřete oznámení pro restartování",
"setting": "nastavení",
"version": "verze",
"title": "název",
"filter_one": "filtr",
"filter_few": "filtry",
"filter_other": "filtrů",
"filters": "filtry",
"create": "vytvořit",
"bitrate": "datový tok",
"saveAndReplace": "uložit a nahradit",
"action_one": "akce",
"action_few": "akce",
"action_other": "akcí",
"playerMustBePaused": "přehrávač musí být pozastaven",
"confirm": "potvrdit",
"resetToDefault": "resetovat na výchozí",
"home": "domů",
"comingSoon": "již brzy…",
"reset": "resetovat",
"channel_one": "kanál",
"channel_few": "kanály",
"channel_other": "kanálů",
"disable": "vypnout",
"sortOrder": "pořadí",
"none": "žádný",
"menu": "nabídka",
"restartRequired": "vyžadován restart",
"previousSong": "předchozí $t(entity.track_one)",
"noResultsFromQuery": "nebyly nalezeny žádné výsledky",
"quit": "ukončit",
"expand": "rozbalit",
"search": "hledat",
"saveAs": "uložit jako",
"disc": "disk",
"yes": "ano",
"random": "náhodně",
"size": "velikost",
"biography": "biografie",
"note": "poznámka"
},
"table": {
"config": {
"view": {
"card": "karta",
"table": "tabulka",
"poster": "plakát"
},
"general": {
"displayType": "typ zobrazení",
"gap": "$t(common.gap)",
"tableColumns": "sloupce tabulky",
"autoFitColumns": "automaticky přizpůsobit sloupce",
"size": "$t(common.size)"
},
"label": {
"releaseDate": "datum vydání",
"title": "$t(common.title)",
"duration": "$t(common.duration)",
"titleCombined": "$t(common.title) (kombinovaný)",
"dateAdded": "datum přidání",
"size": "$t(common.size)",
"bpm": "$t(common.bpm)",
"lastPlayed": "naposledy přehráno",
"trackNumber": "číslo stopy",
"rowIndex": "index řádku",
"rating": "$t(common.rating)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
"note": "$t(common.note)",
"biography": "$t(common.biography)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"channels": "$t(common.channel_other)",
"playCount": "počet přehrání",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action_other)",
"genre": "$t(entity.genre_one)",
"discNumber": "číslo disku",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
}
},
"column": {
"comment": "komentář",
"album": "album",
"rating": "hodnocení",
"favorite": "oblíbené",
"playCount": "přehrání",
"albumCount": "$t(entity.album_other)",
"releaseYear": "rok",
"lastPlayed": "naposledy přehráno",
"biography": "biografie",
"releaseDate": "datum vydání",
"bitrate": "datový tok",
"title": "název",
"bpm": "bpm",
"dateAdded": "datum přidání",
"artist": "$t(entity.artist_one)",
"songCount": "$t(entity.track_other)",
"trackNumber": "skladba",
"genre": "$t(entity.genre_one)",
"albumArtist": "umělec alba",
"path": "cesta",
"discNumber": "disk",
"channels": "$t(common.channel_other)"
}
},
"error": {
"remotePortWarning": "restartujte server pro použití nového portu",
"systemFontError": "při pokusu o získání systémových písem se vyskytla chyba",
"playbackError": "při pokusu o přehrání médií se vyskytla chyba",
"endpointNotImplementedError": "endpoint {{endpoint}} není u serveru {{serverType}} implementován",
"remotePortError": "při pokusu o nastavení portu vzdáleného serveru se vyskytla chyba",
"serverRequired": "vyžadován server",
"authenticationFailed": "ověření selhalo",
"apiRouteError": "nepodařilo se přesměrovat žádost",
"genericError": "vyskytla se chyba",
"credentialsRequired": "vyžadovány údaje",
"sessionExpiredError": "vaše relace vypršela",
"remoteEnableError": "při pokusu $t(common.enable) vzdálený server se vyskytla chyba",
"localFontAccessDenied": "přístup k místním písmům zakázán",
"serverNotSelectedError": "není vybrán žádný server",
"remoteDisableError": "při pokusu $t(common.disable) vzdálený server se vyskytla chyba",
"mpvRequired": "vyžadován přehrávač MPV",
"audioDeviceFetchError": "při pokusu o přístup ke zvukovým zařízením se vyskytla chyba",
"invalidServer": "neplatný server",
"loginRateError": "příliš mnoho pokusů o přihlášení, zkuste to znovu za pár vteřin"
},
"filter": {
"mostPlayed": "nejvíce přehráváno",
"comment": "komentář",
"playCount": "počet přehrání",
"recentlyUpdated": "nedávno upraveno",
"channels": "$t(common.channel_other)",
"isCompilation": "je kompilace",
"recentlyPlayed": "nedávno přehráno",
"isRated": "je hodnoceno",
"owner": "$t(common.owner)",
"title": "název",
"rating": "hodnocení",
"search": "hledat",
"bitrate": "datový tok",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "nedávno přidáno",
"note": "poznámka",
"name": "název",
"dateAdded": "datum přidání",
"releaseDate": "datum vydání",
"albumCount": "počet $t(entity.album_other)",
"communityRating": "komunitní hodnocení",
"path": "cesta",
"favorited": "oblíbené",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "je nedávno přehráno",
"isFavorited": "je oblíbené",
"bpm": "bpm",
"releaseYear": "rok vydání",
"id": "id",
"disc": "disk",
"biography": "biografie",
"songCount": "počet skladeb",
"artist": "$t(entity.artist_one)",
"duration": "trvání",
"isPublic": "je veřejné",
"random": "náhodně",
"lastPlayed": "naposledy přehráno",
"toYear": "do roku",
"fromYear": "z roku",
"criticRating": "hodnocení kritiků",
"album": "$t(entity.album_one)",
"trackNumber": "skladba"
},
"page": {
"sidebar": {
"nowPlaying": "právě hraje",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
},
"fullscreenPlayer": {
"config": {
"showLyricMatch": "zobrazit shodu textů",
"dynamicBackground": "dynamické pozadí",
"synchronized": "synchronizováno",
"followCurrentLyric": "následovat aktuální text",
"opacity": "neprůhlednost",
"lyricSize": "velikost textů",
"showLyricProvider": "zobrazit poskytovatele textů",
"unsynchronized": "nesynchronizováno",
"lyricAlignment": "zarovnání textů",
"useImageAspectRatio": "použít poměr stran obrázku",
"lyricGap": "mezera textů"
},
"upNext": "další",
"lyrics": "texty",
"related": "související"
},
"appMenu": {
"selectServer": "vybrat server",
"version": "verze {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "správce serverů",
"expandSidebar": "rozbalit postranní panel",
"collapseSidebar": "sbalit postranní panel",
"openBrowserDevtools": "otevřít vývojářské nástroje",
"quit": "$t(common.quit)",
"goBack": "přejít zpět",
"goForward": "přejít vpřed"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "vybráno {{count}}",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"home": {
"mostPlayed": "nejpřehrávanější",
"newlyAdded": "nově přidáno",
"title": "$t(common.home)",
"explore": "procházet z vaší knihovny",
"recentlyPlayed": "nedávno přehráno"
},
"albumDetail": {
"moreFromArtist": "více od tohoto umělce",
"moreFromGeneric": "více od {{item}}"
},
"setting": {
"playbackTab": "přehrávání",
"generalTab": "obecné",
"hotkeysTab": "klávesové zkratky",
"windowTab": "okno"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"globalSearch": {
"commands": {
"serverCommands": "příkazy serveru",
"goToPage": "přejít na stránku",
"searchFor": "hledání {{query}}"
},
"title": "příkazy"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"form": {
"deletePlaylist": {
"title": "odstranit $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) úspěšně odstraněn",
"input_confirm": "pro potvrzení zadejte název $t(entity.playlist_one)u"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "vytvořit $t(entity.playlist_one)",
"input_public": "veřejné",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) úspěšně vytvořen",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "přidat server",
"input_username": "uživatelské jméno",
"input_url": "adresa url",
"input_password": "heslo",
"input_legacyAuthentication": "zapnout zastaralé ověřování",
"input_name": "název serveru",
"success": "server úspěšně přidán",
"input_savePassword": "uložit heslo",
"ignoreSsl": "ignorovat SSL $t(common.restartRequired)",
"ignoreCors": "ignorovat CORS $t(common.restartRequired)",
"error_savePassword": "při ukládání hesla se vyskytla chyba"
},
"addToPlaylist": {
"success": "přidáno {{message}} $t(entity.song_other) do {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "přidat do $t(entity.playlist_one)",
"input_skipDuplicates": "přeskočit duplicity",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "upravit server",
"success": "server úspěšně upraven"
},
"queryEditor": {
"input_optionMatchAll": "shoda všeho",
"input_optionMatchAny": "shoda libovolného"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "Hledat texty"
},
"editPlaylist": {
"title": "upravit $t(entity.playlist_one)"
}
},
"entity": {
"genre_one": "žánr",
"genre_few": "žánry",
"genre_other": "žánry",
"playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_few": "{{count}} playlisty",
"playlistWithCount_other": "{{count}} playlistů",
"playlist_one": "playlist",
"playlist_few": "playlisty",
"playlist_other": "playlisty",
"artist_one": "umělec",
"artist_few": "umělci",
"artist_other": "umělci",
"folderWithCount_one": "{{count}} složka",
"folderWithCount_few": "{{count}} složky",
"folderWithCount_other": "{{count}} složek",
"albumArtist_one": "umělec alba",
"albumArtist_few": "umělci alba",
"albumArtist_other": "umělců alba",
"track_one": "skladba",
"track_few": "skladby",
"track_other": "skladby",
"albumArtistCount_one": "{{count}} umělec alba",
"albumArtistCount_few": "{{count}} umělci alba",
"albumArtistCount_other": "{{count}} umělců alba",
"albumWithCount_one": "{{count}} album",
"albumWithCount_few": "{{count}} alba",
"albumWithCount_other": "{{count}} alb",
"favorite_one": "oblíbená",
"favorite_few": "oblíbené",
"favorite_other": "oblíbených",
"artistWithCount_one": "{{count}} umělec",
"artistWithCount_few": "{{count}} umělci",
"artistWithCount_other": "{{count}} umělců",
"folder_one": "složka",
"folder_few": "složky",
"folder_other": "složek",
"smartPlaylist": "chytrý $t(entity.playlist_one)",
"album_one": "album",
"album_few": "alba",
"album_other": "alba",
"genreWithCount_one": "{{count}} žánr",
"genreWithCount_few": "{{count}} žánry",
"genreWithCount_other": "{{count}} žánrů",
"trackWithCount_one": "{{count}} skladba",
"trackWithCount_few": "{{count}} skladby",
"trackWithCount_other": "{{count}} skladeb"
}
}
+549 -4
View File
@@ -1,11 +1,556 @@
{
"action": {
"editPlaylist": "bearbeite $t(entity.playlist_one)",
"clearQueue": "warteschlange löschen",
"editPlaylist": "bearbeiten $t(entity.playlist_one)",
"clearQueue": "Warteschlange löschen",
"addToFavorites": "hinzufügen zu $t(entity.favorite_other)",
"addToPlaylist": "hinzufügen zu $t(entity.playlist_one)",
"createPlaylist": "erstelle $t(entity.playlist_one)",
"deletePlaylist": "lösche $t(entity.playlist_one)",
"deselectAll": "alle abwählen"
"deletePlaylist": "löschen $t(entity.playlist_one)",
"deselectAll": "Alle abwählen",
"goToPage": "Gehe zur Seite",
"moveToTop": "Nach Oben",
"moveToBottom": "Nach Unten",
"removeFromPlaylist": "Entfernen von $t(entity.playlist_one)",
"viewPlaylists": "Ansicht $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"removeFromQueue": "Von Warteschlange entfernen",
"setRating": "Bewertung festlegen",
"toggleSmartPlaylistEditor": "Editor $t(entity.smartPlaylist) umschalten",
"removeFromFavorites": "Entfernen von $t(entity.favorite_other)"
},
"common": {
"backward": "rückwärts",
"increase": "erhöhen",
"rating": "Wertung",
"bpm": "bpm",
"refresh": "erneuern",
"unknown": "Unbekannt",
"areYouSure": "Bist Du sicher?",
"edit": "Bearbeiten",
"favorite": "Favorit",
"left": "links",
"save": "Speichern",
"right": "rechts",
"currentSong": "momentaner $t(entity.track_one)",
"collapse": "Zusammenklappen",
"trackNumber": "Track",
"descending": "absteigend",
"add": "Hinzufügen",
"gap": "Lücke",
"ascending": "aufsteigend",
"dismiss": "Verwerfen",
"year": "Jahr",
"manage": "Verwalten",
"limit": "Limit",
"minimize": "minimieren",
"modified": "geändert",
"duration": "Laufzeit",
"name": "Name",
"maximize": "maximieren",
"decrease": "verringern",
"ok": "okay",
"description": "Beschreibung",
"configure": "Konfigurieren",
"path": "Pfad",
"center": "Zentrieren",
"no": "Nein",
"owner": "Eigentümer",
"enable": "Aktivieren",
"clear": "Bereinigen",
"forward": "vorwärts",
"delete": "Löschen",
"cancel": "Abbrechen",
"forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten",
"setting": "Einstellung",
"version": "Version",
"title": "Titel",
"filter_one": "Filter",
"filter_other": "Filter",
"filters": "Filter",
"create": "Erstellen",
"bitrate": "Bitrate",
"saveAndReplace": "Speichern und Ersetzen",
"action_one": "Aktion",
"action_other": "Aktionen",
"playerMustBePaused": "Player muss pausiert sein",
"confirm": "Bestätigen",
"resetToDefault": "Auf Standard zurücksetzen",
"home": "Home",
"comingSoon": "Kommt bald…",
"reset": "zurücksetzen",
"channel_one": "Kanal",
"channel_other": "Kanäle",
"disable": "Deaktivieren",
"sortOrder": "Reihenfolge",
"none": "keine",
"menu": "Menü",
"restartRequired": "Neustart benötigt",
"previousSong": "vorheriger $t(entity.track_one)",
"noResultsFromQuery": "Die Abfrage brachte keine Ergebnisse",
"quit": "Verlassen",
"expand": "expandieren",
"search": "Suchen",
"saveAs": "Speichern unter",
"disc": "Disk",
"yes": "Ja",
"random": "zufällig",
"size": "Größe",
"biography": "Biografie",
"note": "Hinweis"
},
"error": {
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
"systemFontError": "Beim Versuch, Systemschriftarten abzurufen, ist ein Fehler aufgetreten",
"playbackError": "Beim Versuch, das Medium abzuspielen, ist ein Fehler aufgetreten",
"endpointNotImplementedError": "Endgerät {{endpoint}} ist nicht für {{serverType}} implementiert",
"remotePortError": "Beim Versuch, den Remote-Server-Port festzulegen, ist ein Fehler aufgetreten",
"serverRequired": "Server benötigt",
"authenticationFailed": "Authentifizierung fehlgeschlagen",
"apiRouteError": "Anforderung kann nicht weitergeleitet werden",
"genericError": "Ein Fehler ist aufgetreten",
"credentialsRequired": "Anmeldeinformationen erforderlich",
"sessionExpiredError": "Deine Sitzung ist abgelaufen",
"remoteEnableError": "Beim Versuch, den Remote-Server mit $t(common.enable), ist ein Fehler aufgetreten",
"localFontAccessDenied": "Zugriff auf lokale Schriftarten verweigert",
"serverNotSelectedError": "Kein Server ausgewählt",
"remoteDisableError": "Beim Versuch, den Remote-Server mit $t(common.disable), ist ein Fehler aufgetreten",
"mpvRequired": "MPV benötigt",
"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"
},
"filter": {
"mostPlayed": "Meist gespielt",
"comment": "Kommentar",
"playCount": "Anzahl abgespielt",
"recentlyUpdated": "kürzlich aktualisiert",
"isCompilation": "ist Zusammenstellung",
"recentlyPlayed": "kürzlich gespielt",
"isRated": "ist bewertet",
"title": "Titel",
"rating": "Bewertung",
"search": "Suche",
"bitrate": "Bitrate",
"recentlyAdded": "kürzlich hinzugefügt",
"note": "Hinweis",
"name": "Name",
"dateAdded": "Datum hinzugefügt",
"releaseDate": "Veröffentlichungsdatum",
"albumCount": "$t(entity.album_other) Anzahl",
"communityRating": "Community-Wertung",
"path": "Pfad",
"favorited": "favorisiert",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "wurde kürzlich gespielt",
"isFavorited": "wird favorisiert",
"bpm": "bpm",
"releaseYear": "Erscheinungsjahr",
"id": "ID",
"disc": "Disk",
"biography": "Biografie",
"songCount": "Anzahl Lieder",
"duration": "Dauer",
"isPublic": "ist öffentlich",
"random": "zufällig",
"lastPlayed": "Zuletzt gespielt",
"toYear": "bis Jahr",
"fromYear": "ab Jahr",
"criticRating": "Kritikerbewertung",
"album": "$t(entity.album_one)",
"trackNumber": "Track",
"channels": "$t(common.channel_other)",
"owner": "$t(common.owner)",
"genre": "$t(entity.genre_one)",
"artist": "$t(entity.artist_one)"
},
"form": {
"deletePlaylist": {
"title": "Lösche $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) erfolgreich gelöscht",
"input_confirm": "Geben Sie zur Bestätigung den Namen von $t(entity.playlist_one) ein"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "Erstellen $t(entity.playlist_one)",
"input_public": "öffentlich",
"success": "$t(entity.playlist_one) erfolgreich erstellt",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "Server hinzufügen",
"input_username": "Benutzername",
"input_url": "URL",
"input_password": "Passwort",
"input_legacyAuthentication": "Aktivieren der Legacy-Authentifizierung",
"input_name": "Server Name",
"success": "Server erfolgreich hinzugefügt",
"input_savePassword": "Passwort speichern",
"ignoreSsl": "ignoriere ssl $t(common.restartRequired)",
"ignoreCors": "ignoriere cors $t(common.restartRequired)",
"error_savePassword": "Beim Versuch, das Passwort zu speichern, ist ein Fehler aufgetreten"
},
"addToPlaylist": {
"success": "{{message}} $t(entity.song_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt",
"title": "Zu $t(entity.playlist_one) hinzufügen",
"input_skipDuplicates": "Duplikate überspringen",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "Server aktualisieren",
"success": "Server erfolgreich aktualisiert"
},
"queryEditor": {
"input_optionMatchAll": "Treffer Alle",
"input_optionMatchAny": "Treffer Einige"
},
"editPlaylist": {
"title": "Bearbeite $t(entity.playlist_one)"
},
"lyricSearch": {
"title": "Songtext Suche",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
}
},
"entity": {
"genre_one": "Genre",
"genre_other": "Genres",
"playlistWithCount_one": "{{count}} Wiedergabeliste",
"playlistWithCount_other": "{{count}} Wiedergabelisten",
"playlist_one": "Wiedergabeliste",
"playlist_other": "Wiedergabelisten",
"artist_one": "Interpret",
"artist_other": "Interpreten",
"folderWithCount_one": "{{count}} Verzeichnis",
"folderWithCount_other": "{{count}} Verzeichnisse",
"albumArtist_one": "Album Interpret",
"albumArtist_other": "Album Interpreten",
"track_one": "Track",
"track_other": "Tracks",
"albumArtistCount_one": "{{count}} Album Interpret",
"albumArtistCount_other": "{{count}} Album Interpreten",
"albumWithCount_one": "{{count}} Album",
"albumWithCount_other": "{{count}} Alben",
"favorite_one": "Favorit",
"favorite_other": "Favoriten",
"artistWithCount_one": "{{count}} Interpret",
"artistWithCount_other": "{{count}} Interpreten",
"folder_one": "Verzeichnis",
"folder_other": "Verzeichnisse",
"album_one": "Album",
"album_other": "Alben",
"genreWithCount_one": "{{count}} Genre",
"genreWithCount_other": "{{count}} Genres",
"trackWithCount_one": "{{count}} Track",
"trackWithCount_other": "{{count}} Tracks",
"smartPlaylist": "Smart $t(entity.playlist_one)"
},
"table": {
"config": {
"view": {
"table": "Tabelle"
},
"general": {
"tableColumns": "Tabellenspalten"
}
},
"column": {
"releaseYear": "Jahr",
"biography": "Biografie",
"releaseDate": "Veröffentlichungsdatum",
"bitrate": "Bitrate",
"title": "Titel",
"path": "Pfad"
}
},
"page": {
"fullscreenPlayer": {
"config": {
"showLyricMatch": "Textübereinstimmung anzeigen",
"dynamicBackground": "Dynamischer Hintergrund",
"synchronized": "synchronisiert",
"followCurrentLyric": "dem Songtext folgen",
"opacity": "Deckkraft",
"lyricSize": "Songtext Größe",
"showLyricProvider": "Songtext-Anbieter anzeigen",
"unsynchronized": "nicht synchronisiert",
"lyricAlignment": "Songtext Ausrichtung",
"useImageAspectRatio": "Bildseitenverhältnis verwenden",
"lyricGap": "Songtext Lücke"
},
"upNext": "als nächstes",
"lyrics": "Songtexte",
"related": "Ähnliche"
},
"appMenu": {
"selectServer": "Server auswählen",
"version": "Version {{version}}",
"manageServers": "Server verwalten",
"expandSidebar": "Seitenleiste erweitern",
"collapseSidebar": "Seitenleiste einklappen",
"openBrowserDevtools": "Browser Entwicklungswerkzeuge öffnen",
"goBack": "Gehe zurück",
"goForward": "Gehe vorwärts",
"settings": "$t(common.setting_other)",
"quit": "$t(common.quit)"
},
"home": {
"mostPlayed": "Meist gespielt",
"newlyAdded": "Neu hinzugefügte Veröffentlichungen",
"explore": "Entdecken Sie Ihre Bibliothek",
"recentlyPlayed": "Kürzlich gespielt",
"title": "$t(common.home)"
},
"albumDetail": {
"moreFromArtist": "Mehr von diesem $t(entity.genre_one)",
"moreFromGeneric": "Mehr von {{item}}"
},
"globalSearch": {
"commands": {
"serverCommands": "Serverbefehle",
"goToPage": "Gehe zur Seite",
"searchFor": "Suche nach {{query}}"
},
"title": "Befehle"
},
"contextMenu": {
"numberSelected": "{{count}} Ausgewählte",
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"sidebar": {
"nowPlaying": "läuft gerade",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
},
"setting": {
"playbackTab": "Wiedergabe",
"generalTab": "allgemein",
"hotkeysTab": "Kurzbefehle",
"windowTab": "Fenster"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"player": {
"next": "Nächster",
"addNext": "Als Nächstes einfügen",
"play": "Abspielen",
"muted": "Stummgeschaltet",
"addLast": "Ans Ende einzufügen",
"mute": "Stumm",
"playRandom": "Zufällige Wiedergabe",
"previous": "Vorheriger",
"favorite": "Favorit",
"playbackFetchNoResults": "Keine Lieder gefunden",
"playbackFetchInProgress": "Lieder werden geladen…",
"playbackSpeed": "Wiedergabegeschwindigkeit",
"playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen",
"queue_clear": "Bereinige Warteschlange",
"repeat_all": "Alle wiederholen",
"repeat": "Wiederholen",
"queue_remove": "Ausgewählte entfernen",
"shuffle": "Zufallswiedergabe",
"repeat_off": "Nicht wiederholen",
"queue_moveToTop": "Ausgewählte nach unten verschieben",
"queue_moveToBottom": "Ausgewählte nach oben verschieben",
"shuffle_off": "Zufallswiedergabe deaktiviert",
"stop": "Stopp",
"toggleFullscreenPlayer": "Vollbildmodus",
"skip_back": "Zurückspulen",
"pause": "Pause",
"unfavorite": "Aus Favoriten entfernen",
"skip_forward": "Vorspulen",
"skip": "Überspringen"
},
"setting": {
"audioDevice_description": "Wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer).",
"audioExclusiveMode": "Audio Exklusiver Modus",
"audioDevice": "Audiogerät",
"accentColor": "Akzentfarbe",
"accentColor_description": "Legt die Akzentfarbe für die Anwendung fest",
"applicationHotkeys": "Tastenkombinationen der Anwendung",
"applicationHotkeys_description": "Konfiguriere die Tastenkombinationen der Anwendung. Setze einen Haken, um die Tastenkombination global zu verwenden (nur für die Desktopanwendung)",
"crossfadeStyle_description": "Wählen Sie Art des Überblendungseffekts aus, welcher für den Audioplayer verwendet werden soll",
"discordIdleStatus_description": "Wenn aktiviert wird der Rich Presence Status aktiviert, wenn sich der Player im Leerlauf befindet",
"crossfadeStyle": "Art der Überblendung",
"audioExclusiveMode_description": "Aktivieren Sie den exklusiven Ausgabemodus. In diesem Modus ist das System normalerweise gesperrt und nur MPV ist in der Lage Audio ausgeben",
"disableLibraryUpdateOnStartup": "Beim Start nicht nach neuen Versionen suchen",
"discordApplicationId_description": "Die Application-ID für {{discord}} Rich Presence (Standard: {{defaultId}})",
"audioPlayer_description": "Wählen Sie den Audioplayer aus, der für die Wiedergabe verwendet werden soll",
"disableAutomaticUpdates": "Automatische Updates deaktivieren",
"crossfadeDuration_description": "Legt die Dauer der Überblendung fest",
"customFontPath": "Benutzerdefinierter Pfad für Schriftarten",
"crossfadeDuration": "Dauer der Überblendung",
"discordIdleStatus": "Rich Presence Status im Leerlauf",
"audioPlayer": "Audio-Player",
"discordApplicationId": "{{discord}} Anwendungs ID",
"customFontPath_description": "Legt den Pfad zur benutzerdefinierten Schriftart fest, welche für die Anwendung verwendet werden soll",
"discordRichPresence": "{{discord}} Rich Presence",
"remotePort_description": "Legt den Port des Fernsteuerungsserver fest",
"hotkey_skipBackward": "rückwärts springen",
"replayGainMode_description": "Passen Sie die Lautstärkeverstärkung entsprechend den in den Dateimetadaten gespeicherten {{ReplayGain}}-Werten an",
"volumeWheelStep_description": "die Lautstärke, die beim Scrollen des Mausrads auf dem Lautstärkeregler geändert werden soll",
"theme_description": "Legt das für die Anwendung zu verwendende Thema fest",
"hotkey_playbackPause": "Pause",
"sidebarCollapsedNavigation_description": "Zeigt die Navigation in der minimierten Seitenleiste an oder verbirgt sie",
"mpvExecutablePath_help": "eine pro Zeile",
"hotkey_volumeUp": "Lauter",
"skipDuration": "Sprung Dauer",
"showSkipButtons": "Schaltflächen zum Überspringen anzeigen",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "minimale Scrobble-Dauer (Prozentsatz)",
"lyricFetch": "Songtexte aus dem Internet abrufen",
"scrobble": "Scrobbeln",
"skipDuration_description": "Legt die zu überspringende Dauer fest, wenn die Überspringen-Schaltflächen in der Player-Leiste verwendet werden",
"mpvExecutablePath_description": "Legt den Pfad zur ausführbaren MPV-Datei fest",
"replayGainClipping_description": "Verhindern Sie durch {{ReplayGain}} verursachtes Clipping, indem Sie die Verstärkung automatisch verringern",
"replayGainPreamp": "{{ReplayGain}} Vorverstärker (db)",
"hotkey_favoriteCurrentSong": "Favorit $t(common.currentSong)",
"sampleRate": "Abtastrate",
"sidePlayQueueStyle_optionAttached": "angefügt",
"sidebarConfiguration": "Seitenleistenkonfiguration",
"sampleRate_description": "Wählen Sie die auszugebende Abtastrate aus, wenn sich die ausgewählte Abtastfrequenz von der des aktuellen Mediums unterscheidet",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_zoomIn": "Hineinzoomen",
"scrobble_description": "Scrobble wird auf Ihrem Medienserver abgespielt",
"hotkey_browserForward": "Browser vor",
"hotkey_playbackPlayPause": "Wiedergabe / Pause",
"hotkey_rate1": "Bewertung 1 Stern",
"hotkey_skipForward": "vorwärts springen",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"minimizeToTray_description": "Minimieren der Anwendung in die Taskleiste",
"hotkey_playbackPlay": "Wiedergabe",
"hotkey_volumeDown": "Leiser",
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) aus Favoriten entfernen",
"globalMediaHotkeys": "Globale Medien Kurzbefehle",
"hotkey_globalSearch": "Globale Suche",
"gaplessAudio_description": "Legt die lückenlose Audioeinstellung für MPV fest",
"remoteUsername_description": "Legt den Benutzernamen für den Fernsteuerungsserver fest. Wenn sowohl Benutzername als auch Passwort leer sind, wird die Authentifizierung deaktiviert",
"hotkey_favoritePreviousSong": "Favorit $t(common.previousSong)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"lyricOffset": "Liedtext-Versatz (ms)",
"themeDark_description": "Legt das dunkle Design fest, das für die Anwendung verwendet werden soll",
"remotePassword": "Passwort des Fernsteuerungsservers",
"lyricFetchProvider": "Anbieter, von denen Liedtexte abgerufen werden können",
"language_description": "Legt die Sprache für die Anwendung fest $t(common.restartRequired)",
"playbackStyle_optionCrossFade": "Überblendung",
"hotkey_rate3": "Bewertung 3 Sterne",
"mpvExtraParameters": "mpv Parameter",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "Legt das helle Thema fest, das für die Anwendung verwendet werden soll",
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
"hotkey_localSearch": "Suche auf Seite",
"hotkey_toggleQueue": "Warteschlange umschalten",
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein eindeutiges Passwort verwenden, das Ihnen egal ist",
"hotkey_rate5": "Bewertung 5 Sterne",
"hotkey_playbackPrevious": "Vorheriger Track",
"showSkipButtons_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
"language": "Sprache",
"playbackStyle": "Wiedergabestil",
"hotkey_toggleShuffle": "Zufallswiedergabe umschalten",
"theme": "Thema",
"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 der Wiedergabeschaltfläche fest, wenn Songs zur Warteschlange hinzugefügt werden",
"minimumScrobblePercentage_description": "Der Mindestprozentsatz des Songs, der gespielt werden muss, bevor er gescrobbelt wird",
"hotkey_rate4": "Bewertung 4 Sterne",
"showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
"savePlayQueue": "Wiedergabe-Warteschlange speichern",
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, die das Lied abspielen muss, bevor es gescrobbelt wird",
"skipPlaylistPage_description": "Gehen Sie beim Navigieren zu einer Wiedergabeliste zur Titelseite der Wiedergabeliste 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 bereitstellen",
"playButtonBehavior": "Verhalten der Wiedergabetaste",
"volumeWheelStep": "Lautstärkeregler Stufe",
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
"sidePlayQueueStyle_description": "Legt den Stil der Wiedergabewarteliste in der Seitenleiste fest",
"replayGainMode": "{{ReplayGain}} Modus",
"playbackStyle_optionNormal": "Normal",
"windowBarStyle": "Fensterleistenstil",
"replayGainFallback_description": "Verstärkung in db, die angewendet werden soll, wenn die Datei keine {{ReplayGain}}-Tags hat",
"replayGainPreamp_description": "Passen Sie die Vorverstärkerverstärkung an, die auf die {{ReplayGain}}-Werte angewendet wird",
"hotkey_toggleRepeat": "Wiederholung umschalten",
"lyricOffset_description": "Versetzen Sie den Liedtext um die angegebene Anzahl von Millisekunden",
"sidebarConfiguration_description": "Wählen Sie die Elemente und die Reihenfolge aus, in der sie in der Seitenleiste angezeigt werden",
"remotePort": "Port des Fernsteuerungsserver",
"hotkey_playbackNext": "Nächster Track",
"useSystemTheme_description": "der systemdefinierten Hell oder Dunkel Präferenz folgen",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "Songtexte aus verschiedenen Internetquellen abrufen",
"lyricFetchProvider_description": "Wählen Sie die Anbieter aus, von denen Sie Liedtexte abrufen möchten. Die Reihenfolge der Anbieter ist die Reihenfolge, in der sie abgefragt werden",
"globalMediaHotkeys_description": "Aktivieren oder deaktivieren Sie die Verwendung der Medien-Kurzbefehle Ihres Systems zur Steuerung der Wiedergabe",
"hotkey_zoomOut": "Herauszoomen",
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) aus Favoriten entfernen",
"hotkey_rate0": "Bewertung löschen",
"hotkey_volumeMute": "Lautstärke stumm",
"remoteUsername": "Benutzername des Fernsteuerungsserver",
"hotkey_browserBack": "Browser zurück",
"showSkipButton": "Schaltflächen zum Überspringen anzeigen",
"sidebarPlaylistList": "Seitenleiste Playlisten-Liste",
"minimizeToTray": "Zur Taskleiste minimieren",
"skipPlaylistPage": "Playlisten-Seite überspringen",
"themeDark": "Thema (dunkel)",
"sidebarCollapsedNavigation": "Navigation in der Seitenleiste (komprimiert)",
"gaplessAudio_optionWeak": "schwach (empfohlen)",
"minimumScrobbleSeconds": "minimales Scrobble (Sekunden)",
"hotkey_playbackStop": "Stoppen",
"savePlayQueue_description": "Speichert Wiedergabewarteschlange, wenn die Anwendung geschlossen wird, und stellt sie wieder her, wenn die Anwendung geöffnet wird",
"useSystemTheme": "Systemdesign verwenden",
"enableRemote_description": "Aktiviere den eingebauten Webserver, um die Anwendung von anderen Geräten aus zu steuern",
"fontType_optionSystem": "Systemschriftart",
"discordUpdateInterval": "{{discord}} Rich Presence Aktualisierungsintervall",
"fontType_optionBuiltIn": "Eingebaute Schriftart",
"gaplessAudio": "Unterbrechungsfreie Wiedergabe",
"exitToTray_description": "Die Anwendung beim Schließen in die Taskleiste minimieren",
"followLyric_description": "Der Songtext scrollt automatisch mir der Wiedergabe",
"discordUpdateInterval_description": "Zeit in Sekunden zwischen den Statusupdates (Minimum: 15s)",
"fontType_optionCustom": "Benutzerdefinierte Schriftart",
"font": "Schriftart",
"exitToTray": "In die Taskleiste minimieren",
"enableRemote": "Server für Fernzugriff aktivieren",
"floatingQueueArea": "Beim Darüberfahren schwebende Warteschlange anzeigen",
"fontType": "Schriftartenquelle",
"followLyric": "Songtext synchronisieren",
"floatingQueueArea_description": "Zeige ein Icon auf der rechten Seite, um beim Darüberfahren die Wartschlange anzuzeigen",
"font_description": "Wähle die Schriftart für die Anwendung",
"themeLight": "Thema (hell)",
"sidePlayQueueStyle_optionDetached": "lösgelöst",
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste"
}
}
+3 -2
View File
@@ -144,6 +144,7 @@
"localFontAccessDenied": "access denied to local fonts",
"loginRateError": "too many login attempts, please try again in a few seconds",
"mpvRequired": "MPV required",
"networkError": "a network error occurred",
"playbackError": "an error occurred when trying to play the media",
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
"remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server",
@@ -253,7 +254,7 @@
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "more from this $t(entity.genre_one)",
"moreFromArtist": "more from this $t(entity.artist_one)",
"moreFromGeneric": "more from {{item}}"
},
"albumList": {
@@ -551,7 +552,7 @@
"column": {
"album": "album",
"albumArtist": "album artist",
"albumCount": "$t(entity.album_other)",
"albumCount": "$t(entity.album_one)",
"artist": "$t(entity.artist_one)",
"biography": "biography",
"bitrate": "bitrate",
+8 -8
View File
@@ -36,7 +36,7 @@
"hotkey_skipBackward": "saltar hacia atrás",
"replayGainMode_description": "ajusta el volumen de ganancia acorde a los valores de {{ReplayGain}} almacenados en los metadatos del archivo",
"audioDevice_description": "selecciona el dispositivo de audio para usar en la reproducción (solo reproductor web)",
"theme_description": "establece el tema a usar para la aplicación",
"theme_description": "establece el tema a usar por la aplicación",
"hotkey_playbackPause": "pausa",
"replayGainFallback": "{{ReplayGain}} alternativa",
"sidebarCollapsedNavigation_description": "mostrar u ocultar la navegación en la barra lateral contraída",
@@ -95,17 +95,17 @@
"lyricOffset": "desfase de letra (ms)",
"discordUpdateInterval_description": "el tiempo en segundos entre cada actualización (mínimo 15 segundos)",
"fontType_optionCustom": "fuente personalizada",
"themeDark_description": "establece el tema oscuro a usar para la aplicación",
"themeDark_description": "establece el tema oscuro a usar por la aplicación",
"audioExclusiveMode": "modo de audio exclusivo",
"remotePassword": "contraseña del control remoto del servidor",
"lyricFetchProvider": "proveedores para buscar letras",
"language_description": "establece el idioma para la aplicación ($t(common.restartRequired))",
"language_description": "establece el idioma de la aplicación ($t(common.restartRequired))",
"playbackStyle_optionCrossFade": "crossfade",
"hotkey_rate3": "calificar con 3 estrellas",
"font": "fuente",
"mpvExtraParameters": "parámetros de mpv",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "establece el tema luminoso a usar para la aplicación",
"themeLight_description": "establece el tema luminoso a usar por la aplicación",
"hotkey_toggleFullScreenPlayer": "cambia el reproductor a pantalla completa",
"hotkey_localSearch": "búsqueda en la página",
"hotkey_toggleQueue": "cambia la cola",
@@ -178,17 +178,17 @@
"hotkey_playbackStop": "parar",
"discordRichPresence": "estado de actividad de {{discord}}",
"font_description": "establece la fuente a usar por la aplicación",
"savePlayQueue_description": "guarda la cola de reproducción cuando la aplicación es cerrada y la restaura cuando la aplicación es abierta",
"savePlayQueue_description": "guarda la cola de reproducción cuando se cierra la aplicación y la restaura cuando se abre",
"useSystemTheme": "usar tema del sistema",
"volumeWheelStep_description": "la cantidad de volumen a cambiar cuando se desplaza la rueda del ratón en el control deslizante del volumen",
"zoom": "porcentaje de zoom",
"zoom_description": "establece el porcentaje de zoom para la aplicación",
"zoom_description": "establece el porcentaje de zoom de la aplicación",
"volumeWheelStep": "paso de rueda del volumen",
"windowBarStyle": "estilo de la barra de ventana",
"windowBarStyle_description": "selecciona el estilo de la barra de ventana",
"skipPlaylistPage_description": "cuando se navega a una lista de reproducción, se va a la página de lista de canciones de la lista de reproducción en lugar de a la página por defecto",
"accentColor": "color de realce",
"accentColor_description": "establece el color de realce para la aplicación",
"accentColor_description": "establece el color de realce de la aplicación",
"skipPlaylistPage": "saltar página de lista de reproducción",
"hotkey_browserForward": "avance",
"hotkey_browserBack": "retroceso"
@@ -430,7 +430,7 @@
"related": "relacionado"
},
"albumDetail": {
"moreFromArtist": "más de este $t(entity.genre_one)",
"moreFromArtist": "más de este $t(entity.artist_one)",
"moreFromGeneric": "más de {{item}}"
},
"setting": {
+188 -77
View File
@@ -4,54 +4,55 @@
"stop": "stop",
"repeat": "répéter",
"queue_remove": "effacer la sélection",
"playRandom": "jouer au hasard",
"playRandom": "lecture aléatoire",
"skip": "sauter",
"previous": "précédant",
"toggleFullscreenPlayer": "basculer le lecteur plein écran",
"skip_back": "sauter en arrière",
"toggleFullscreenPlayer": "plein écran",
"skip_back": "reculer",
"favorite": "favori",
"next": "suivant",
"shuffle": "lecture aléatoire",
"shuffle": "aléatoire",
"playbackFetchNoResults": "aucune chansons trouvées",
"playbackFetchInProgress": "chargement des chansons…",
"addNext": "ajouter ensuite",
"playbackSpeed": "vitesse de lecture",
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
"play": "jouer",
"play": "lecture",
"repeat_off": "répétition désactivée",
"queue_clear": "effacer la file d'attente",
"muted": "en sourdine",
"queue_moveToTop": "déplacer la sélection vers le bas",
"queue_moveToBottom": "déplacer la sélection vers le haut",
"shuffle_off": "lecture aléatoire désactivée",
"shuffle_off": "aléatoire désactivée",
"addLast": "ajouter en dernier",
"mute": "muet",
"skip_forward": "suivant",
"pause": "pause"
"skip_forward": "avancer",
"pause": "pause",
"unfavorite": "dé-favori"
},
"action": {
"editPlaylist": "éditer $t(entity.playlist_one)",
"goToPage": "aller à la page",
"moveToTop": "déplacer en haut",
"clearQueue": "effacer la file d'attente",
"addToFavorites": "ajouter à $t(entity.favorite_other)",
"clearQueue": "effacer la liste de lecture",
"addToFavorites": "ajouter aux $t(entity.favorite_other)",
"addToPlaylist": "ajouter à $t(entity.playlist_one)",
"createPlaylist": "créer $t(entity.playlist_one)",
"removeFromPlaylist": "supprimer de $t(entity.playlist_one)",
"removeFromPlaylist": "supprimer des $t(entity.playlist_one)",
"viewPlaylists": "voir $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "supprimer $t(entity.playlist_one)",
"deletePlaylist": "supprimer de $t(entity.playlist_one)",
"removeFromQueue": "retirer de la file d'attente",
"deselectAll": "désélectionner tout",
"moveToBottom": "déplacer en bas",
"setRating": "noter",
"toggleSmartPlaylistEditor": "basculer l'éditeur $t(entity.smartPlaylist)",
"removeFromFavorites": "supprimer de $t(entity.favorite_other)"
"toggleSmartPlaylistEditor": "basculer l'éditeur de $t(entity.smartPlaylist)",
"removeFromFavorites": "retirer des $t(entity.favorite_other)"
},
"common": {
"backward": "reculer",
"increase": "augmenter",
"rating": "notation",
"rating": "note",
"bpm": "bpm",
"refresh": "rafraichir",
"unknown": "inconnu",
@@ -61,14 +62,14 @@
"left": "gauche",
"save": "sauvegarder",
"right": "droite",
"currentSong": "en cours $t(entity.track_one)",
"currentSong": "$t(entity.track_one) actuelle",
"collapse": "réduire",
"trackNumber": "piste",
"descending": "décroisant",
"add": "ajouter",
"gap": "gap",
"gap": "écart",
"ascending": "croissant",
"dismiss": "annuler",
"dismiss": "rejeter",
"year": "année",
"manage": "gérer",
"limit": "limite",
@@ -135,7 +136,7 @@
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
"systemFontError": "une erreur sest produite lors de la tentative dobtenir les polices système",
"playbackError": "une erreur s'est produite lors de la tentative de lecture du média",
"endpointNotImplementedError": "endpoint {{endpoint} is not implemented for {{serverType}}",
"endpointNotImplementedError": "endpoint {{endpoint}} n'est pas implémenté pour {{serverType}}",
"remotePortError": "une erreur s'est produite lors de la tentative de définir le port du serveur distant",
"serverRequired": "serveur requis",
"authenticationFailed": "l'authentification à échoué",
@@ -156,13 +157,13 @@
"mostPlayed": "plus joués",
"playCount": "nombre d'écoutes",
"isCompilation": "est une compilation",
"recentlyPlayed": "récemment joués",
"recentlyPlayed": "récemment joué",
"isRated": "est noté",
"title": "titre",
"rating": "évalué",
"rating": "note",
"search": "recherche",
"bitrate": "bitrate",
"recentlyAdded": "récemment ajoutés",
"recentlyAdded": "ajout récent",
"note": "note",
"name": "nom",
"dateAdded": "date d'ajout",
@@ -170,7 +171,7 @@
"communityRating": "note de la communauté",
"path": "chemin",
"favorited": "favoris",
"isRecentlyPlayed": "est récemment joués",
"isRecentlyPlayed": "est récemment joué",
"isFavorited": "est favoris",
"bpm": "bpm",
"releaseYear": "année de sortie",
@@ -181,29 +182,52 @@
"random": "aléatoire",
"lastPlayed": "dernière joué",
"toYear": "à l'année",
"fromYear": "année",
"fromYear": "depuis l'année",
"criticRating": "note des critiques",
"trackNumber": "piste",
"albumArtist": "$t(entity.albumArtist_one)"
"albumArtist": "$t(entity.albumArtist_one)",
"comment": "commentaire",
"recentlyUpdated": "mis à jour récemment",
"channels": "$t(common.channel_other)",
"owner": "$t(common.owner)",
"genre": "$t(entity.genre_one)",
"albumCount": "$t(entity.album_other) total",
"id": "id",
"artist": "$t(entity.artist_one)",
"isPublic": "est public",
"album": "$t(entity.album_one)"
},
"page": {
"sidebar": {
"nowPlaying": "lecture en cours"
"nowPlaying": "lecture en cours",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
},
"fullscreenPlayer": {
"config": {
"showLyricMatch": "montrer la correspondance des paroles",
"showLyricMatch": "afficher la correspondance des paroles",
"dynamicBackground": "arrière-plan dynamique",
"synchronized": "synchronisé",
"followCurrentLyric": "suivre les paroles actuelles",
"showLyricProvider": "montrer la source des paroles",
"followCurrentLyric": "suivre les paroles",
"showLyricProvider": "afficher la source des paroles",
"unsynchronized": "désynchronisé",
"lyricAlignment": "alignement des paroles",
"useImageAspectRatio": "utiliser le rapport hauteur/largeur de l'image"
"useImageAspectRatio": "utiliser le ratio de l'image",
"opacity": "opacitée",
"lyricSize": "Taille des paroles",
"lyricGap": "espacement des lettres"
},
"upNext": "suivant",
"upNext": "à suivre",
"lyrics": "paroles",
"related": "en rapport"
"related": "similaire"
},
"appMenu": {
"selectServer": "sélectionner le serveur",
@@ -212,22 +236,27 @@
"collapseSidebar": "réduire la barre latérale",
"openBrowserDevtools": "ouvrir les outils de développement du navigateur",
"goBack": "retour arrière",
"goForward": "avancer"
"goForward": "avancer",
"version": "version {{version}}",
"settings": "$t(common.setting_other)",
"quit": "$t(common.quit)"
},
"home": {
"mostPlayed": "plus joués",
"newlyAdded": "versions récemment ajoutés",
"explore": "explorer depuis votre bibliothèque",
"recentlyPlayed": "récemment joués"
"recentlyPlayed": "récemment joué",
"title": "$t(common.home)"
},
"albumDetail": {
"moreFromArtist": "plus de $t(entity.genre_one)",
"moreFromArtist": "plus de $t(entity.artist_one)",
"moreFromGeneric": "plus de {{item}}"
},
"setting": {
"generalTab": "générale",
"hotkeysTab": "raccourci",
"windowTab": "fenêtre"
"windowTab": "fenêtre",
"playbackTab": "lecteur"
},
"globalSearch": {
"commands": {
@@ -238,7 +267,37 @@
"title": "commandes"
},
"contextMenu": {
"numberSelected": "{{count}} sélectionné"
"numberSelected": "{{count}} sélectionné",
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"setting": {
@@ -248,7 +307,7 @@
"audioPlayer_description": "sélectionnez le lecteur audio à utiliser pour la lecture",
"crossfadeDuration_description": "définit la durée du fondu enchaîné",
"audioDevice": "périphérique audio",
"accentColor": "couleur d'accent",
"accentColor": "couleur d'accentuation",
"accentColor_description": "définit la couleur d'accentuation de l'application",
"applicationHotkeys": "raccourcis clavier d'application",
"crossfadeDuration": "durée de fondue enchaînée",
@@ -259,7 +318,7 @@
"disableAutomaticUpdates": "désactiver les mises à jour automatique",
"customFontPath_description": "définit le chemin de police personnalisé pour l'application",
"remotePort_description": "définit le port du serveur de contrôle à distance",
"hotkey_skipBackward": "sauter en arrière",
"hotkey_skipBackward": "reculer",
"hotkey_playbackPause": "pause",
"mpvExecutablePath_help": "line par line",
"hotkey_volumeUp": "monter le volume",
@@ -273,19 +332,19 @@
"mpvExecutablePath_description": "définit le chemin vers l'exécutable mpv",
"hotkey_favoriteCurrentSong": "favori $t(common.currentSong)",
"sampleRate": "taux d'échantillonnage",
"sampleRate_description": "sélectionnez le taux d'échantillonnage de sortie utilisé si la fréquence d'échantillonnage sélectionnée est différente du média actuel",
"sampleRate_description": "sélectionnez le taux d'échantillonnage de sortie utilisé si la fréquence d'échantillonnage sélectionnée est différente de celle du média actuel",
"hotkey_zoomIn": "zoom avant",
"scrobble_description": "scrobble les lectures à votre serveur multimédia",
"hotkey_browserForward": "avancer",
"discordUpdateInterval": "interval de mise à jour de {{discord}} rich presence",
"fontType_optionBuiltIn": "police intégrée",
"hotkey_playbackPlayPause": "play / pause",
"hotkey_playbackPlayPause": "lecture / pause",
"hotkey_rate1": "noter 1 étoile",
"hotkey_skipForward": "avancer",
"disableLibraryUpdateOnStartup": "désactive la vérification de mise à jour au démarrage",
"disableLibraryUpdateOnStartup": "désactive la recherche de mise à jour au démarrage",
"gaplessAudio": "audio sans interruption",
"minimizeToTray_description": "minimise l'application dans la barre d'état système",
"hotkey_playbackPlay": "play",
"minimizeToTray_description": "réduit l'application vers la barre des tâches",
"hotkey_playbackPlay": "lecture",
"hotkey_togglePreviousSongFavorite": "basculer $t(common.previousSong) favoris",
"hotkey_volumeDown": "baisser le volume",
"hotkey_unfavoritePreviousSong": "défavorisé $t(common.previousSong)",
@@ -293,7 +352,7 @@
"hotkey_globalSearch": "recherche globale",
"gaplessAudio_description": "définit les paramètres d'audio sans interruption pour mpv",
"remoteUsername_description": "définit le nom d'utilisateur du serveur de contrôle à distance. si le nom d'utilisateur et le mot de passe sont vides, l'authentification sera désactivée",
"exitToTray_description": "minime l'application dans la barre des tâches",
"exitToTray_description": "quitte l'application vers la barre des tâches",
"followLyric_description": "faire défiler les paroles jusqu'à la position de lecture actuelle",
"hotkey_favoritePreviousSong": "favori $t(common.previousSong)",
"lyricOffset": "décalage des paroles (ms)",
@@ -301,12 +360,12 @@
"fontType_optionCustom": "police personnalisée",
"remotePassword": "mot de passe du serveur de contrôle à distance",
"lyricFetchProvider": "fournisseur depuis lequel récupérer les paroles",
"language_description": "définit la langue de l 'application $t(common.restartRequired)",
"language_description": "définit la langue de l'application $t(common.restartRequired)",
"playbackStyle_optionCrossFade": "fondu enchaîné",
"hotkey_rate3": "noter 3 étoiles",
"font": "police",
"mpvExtraParameters": "paramètres de mpv",
"hotkey_toggleFullScreenPlayer": "basculer le lecteur plein écran",
"hotkey_toggleFullScreenPlayer": "basculer en plein écran",
"hotkey_localSearch": "recherche dans la page",
"hotkey_toggleQueue": "basculer la liste de lecteur",
"remotePassword_description": "définit le mot de passe du serveur de contrôle à distance. Ces identifiants sont par défaut transmises de façon non sécurisées, donc vous devriez utiliser un mot de passe unique dont vous n'avez pas grand-chose à faire",
@@ -317,21 +376,21 @@
"playbackStyle": "style de lecture",
"hotkey_toggleShuffle": "basculer la lecture aléatoire",
"playbackStyle_description": "sélectionnez le style de lecture à utiliser pour le lecteur audio",
"discordRichPresence_description": "activer le status du lecteur dans {{discord}} rich presence. Les images clés sont: {{icon}}, {{playing}}, et {{paused}} ",
"discordRichPresence_description": "active ltat de lecteur dans le status d'activité {{discord}}. Les images clés sont: {{icon}}, {{playing}}, et {{paused}} ",
"mpvExecutablePath": "chemin de l'exécutable mpv",
"hotkey_rate2": "noter 2 étoiles",
"playButtonBehavior_description": "définit le comportement par défaut du bouton play, lors de l'ajout de chanson à la file d'attente",
"minimumScrobblePercentage_description": "le pourcentage minimum de la chanson qui doit être joué avant qu'elle ne soit scrobbleée",
"exitToTray": "minimiser dans la barre des tâches",
"exitToTray": "quitter vers la barre des tâches",
"hotkey_rate4": "noter 4 étoiles",
"enableRemote": "activer le serveur de contrôle à distance",
"showSkipButton_description": "affiche ou cache les boutons suivants et précédents de la barre de lecture",
"savePlayQueue": "sauvegarder la liste de lecture",
"minimumScrobbleSeconds_description": "la durée minimale en secondes de la chanson qui doit être jouée avant qu'elle ne soit scrobbleée",
"fontType_description": "le sélecteur de police intégré sélectionne des polices fourni par Feishin. Police système vous permet de sélectionner des polices fourni par votre système d'éxploitation. personnalisé vous permet de fournir votre propre police",
"fontType_description": "police intégré vous permet de sélectionner une des polices fourni par Feishin. Police système vous permet de sélectionner une des polices fourni par votre système d'éxploitation. personnalisé vous permet de fournir votre propre police",
"playButtonBehavior": "comportement du bouton play",
"playbackStyle_optionNormal": "normale",
"floatingQueueArea": "afficher le zone de survol de la file d'attente flottante",
"floatingQueueArea": "afficher le zone de file d'attente flottante",
"hotkey_toggleRepeat": "basculer la répétition",
"lyricOffset_description": "décale les paroles par le nombre de millisecondes spécifiées",
"fontType": "type de police",
@@ -339,37 +398,37 @@
"hotkey_playbackNext": "piste suivante",
"lyricFetch_description": "récupère les paroles depuis divers source d'internet",
"lyricFetchProvider_description": "sélectionnez le fournisseur auprès desquels récupérer les paroles. l'ordre des fournisseurs et l'ordre dans lequel ils seront interrogés",
"globalMediaHotkeys_description": "active ou désactive l'utilisation des raccourcis clavier multimédia du système pour contrôler la lecture",
"globalMediaHotkeys_description": "active ou désactive l'utilisation des raccourcis clavier multimédia système pour contrôler la lecture",
"followLyric": "suivre les paroles actuelles",
"discordIdleStatus": "afficher le statut d'inactivité de rich presence",
"discordIdleStatus": "afficher l'état d'inactivité dans le status de l'activité",
"hotkey_zoomOut": "zoom arrière",
"hotkey_unfavoriteCurrentSong": "favorisé $t(common.currentSong)",
"hotkey_unfavoriteCurrentSong": "retirer des favoris la $t(common.currentSong)",
"hotkey_rate0": "supprimer la note",
"hotkey_volumeMute": "couper le son",
"hotkey_toggleCurrentSongFavorite": "basculer $t(common.currentSong) favori",
"hotkey_toggleCurrentSongFavorite": "basculer favori de la $t(common.currentSong)",
"remoteUsername": "nom d'utilisateur du serveur de contrôle à distance",
"hotkey_browserBack": "retour arrière",
"showSkipButton": "affiche les boutons suivants et précédents",
"minimizeToTray": "minimise dans la barre des tâches",
"minimizeToTray": "réduire vers la barre des tâches",
"gaplessAudio_optionWeak": "faible (recommandée)",
"minimumScrobbleSeconds": "scrobble minimum (secondes)",
"hotkey_playbackStop": "stop",
"font_description": "définit la police à utiliser pour l'application",
"savePlayQueue_description": "sauvegarde la liste de lecture quand l'application est fermée et restaure quand l'application est ouverte",
"savePlayQueue_description": "sauvegarde la liste de lecture quand l'application est fermée et la restaure quand l'application est ouverte",
"sidebarCollapsedNavigation_description": "affiche ou cache la navigation dans la barre latérale réduite",
"sidebarConfiguration": "configuration de la barre latérale",
"sidebarConfiguration_description": "sélectionnez les items et l'ordre dans lesquels ils seront affichaient dans la barre latérale",
"sidebarPlaylistList": "liste de playlist de la barre latérale",
"sidebarCollapsedNavigation": "navigation de la barre latéral (réduite)",
"skipDuration": "temps de l'avance rapide",
"skipDuration": "durée de l'avance rapide",
"sidePlayQueueStyle_optionAttached": "attaché",
"sidePlayQueueStyle": "style de la liste de lecture latérale",
"sidebarPlaylistList_description": "affiche ou cache la liste de playlist de la barre latérale",
"sidePlayQueueStyle_description": "définit le style de la liste de lecture latérale",
"sidePlayQueueStyle_optionDetached": "détaché",
"volumeWheelStep_description": "la quantité de volume à modifier lors du défilement de la molette de la souris sur le curseur de volume",
"volumeWheelStep_description": "la valeur de volume à modifier lors du défilement de la molette de la souris sur le curseur de volume",
"theme_description": "définit le thème à utiliser pour l'application",
"skipDuration_description": "définit le durée de l'avance rapide, lors de l'utilisation des boutons skip dans la barre de lecture",
"skipDuration_description": "définit le durée du saut rapide, lors de l'utilisation des boutons avancer/reculer de la barre de lecture",
"themeLight": "thème (clair)",
"zoom": "pourcentage de zoom",
"themeDark_description": "définit le thème sombre à utiliser pour l'application",
@@ -377,17 +436,36 @@
"zoom_description": "définit le pourcentage de zoom de l'application",
"theme": "thème",
"skipPlaylistPage_description": "lors de la navigation dans une playlist, aller directement vers le liste des morceaux, au lieu de la page par défaut",
"volumeWheelStep": "marche du curseur de volume",
"volumeWheelStep": "valeur du pas de volume",
"windowBarStyle": "style de la barre de la fenêtre",
"useSystemTheme_description": "suivre le système en termes de thème sombre ou clair",
"useSystemTheme_description": "suivre les préférence du système (sombre ou clair)",
"skipPlaylistPage": "sauter la page de playlist",
"themeDark": "thème (sombre)",
"windowBarStyle_description": "sélectionner le style de la barre de la fenêtre",
"useSystemTheme": "utiliser le thème du système"
"useSystemTheme": "utiliser le thème du système",
"discordApplicationId_description": "l'identifiant de l'application pour le statut d'activité {{discord}} (par défaut à {{defaultId}})",
"audioExclusiveMode": "mode de sortie audio exclusif",
"discordApplicationId": "identifiant d'application {{discord}}",
"floatingQueueArea_description": "afficher une icon flottante sur le côté droit de l'écran pour afficher la liste d'attente",
"discordRichPresence": "status d'activité de {{discord}}",
"playButtonBehavior_optionPlay": "$t(player.play)",
"replayGainMode_optionNone": "$t(common.none)",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"replayGainMode_description": "ajuste le gain de volume accordement à la valeur de {{ReplayGain}} sauvegardé dans les métadonnées du fichier",
"replayGainFallback": "{{ReplayGain}} fallback",
"replayGainClipping_description": "Préviens le clipping causé par {{ReplayGain}} en baissant automatiquement le gain",
"replayGainPreamp": "préamplificateur (dB) de {{ReplayGain}}",
"replayGainClipping": "{{ReplayGain}} clipping",
"replayGainMode": "mode de {{ReplayGain}}",
"replayGainFallback_description": "gain en dB à appliquer si le fichier n'a pas de tag {{ReplayGain}}",
"replayGainPreamp_description": "ajuste le gain de préampli appliqué a la valeur de {{ReplayGain}}"
},
"form": {
"deletePlaylist": {
"title": "supprimer $t(entity.playlist_one)",
"title": "supprimer de $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) supprimée avec succès",
"input_confirm": "taper le nom de la $t(entity.playlist_one) pour confirmer"
},
@@ -407,15 +485,19 @@
"addToPlaylist": {
"success": "{{message}} $t(entity.song_other) ajouté à {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "ajouter à $t(entity.playlist_one)",
"input_skipDuplicates": "sauter les doublons"
"input_skipDuplicates": "sauter les doublons",
"input_playlists": "$t(entity.playlist_other)"
},
"createPlaylist": {
"title": "créer $t(entity.playlist_one)",
"input_public": "publique",
"success": "$t(entity.playlist_one) créée avec succès"
"success": "$t(entity.playlist_one) créée avec succès",
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)"
},
"updateServer": {
"title": "mettre à jour le serveur",
"title": "mise à jour du serveur",
"success": "serveur mis à jour avec succès"
},
"queryEditor": {
@@ -426,7 +508,9 @@
"title": "modifier $t(entity.playlist_one)"
},
"lyricSearch": {
"title": "rechercher parole"
"title": "rechercher parole",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
}
},
"entity": {
@@ -466,7 +550,7 @@
"folder_one": "dossier",
"folder_many": "dossiers",
"folder_other": "dossiers",
"smartPlaylist": "intelligente $t(entity.playlist_one)",
"smartPlaylist": "$t(entity.playlist_one) intelligente",
"album_one": "album",
"album_many": "albums",
"album_other": "albums",
@@ -481,12 +565,15 @@
"config": {
"general": {
"displayType": "Type d'affichage",
"tableColumns": "colonnes du tableau",
"autoFitColumns": "colonnes à ajustement automatique"
"tableColumns": "colonnes de la liste",
"autoFitColumns": "colonnes à ajustement automatique",
"gap": "$t(common.gap)",
"size": "$t(common.size)"
},
"view": {
"table": "tableau",
"poster": "poster"
"table": "liste",
"poster": "poster",
"card": "Carte"
},
"label": {
"releaseDate": "date de sortie",
@@ -496,14 +583,32 @@
"trackNumber": "numéro de piste",
"rowIndex": "index de ligne",
"playCount": "nombre de lecture",
"discNumber": "disque n°"
"discNumber": "disque n°",
"duration": "$t(common.duration)",
"bpm": "$t(common.bpm)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
"biography": "$t(common.biography)",
"channels": "$t(common.channel_other)",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action_other)",
"favorite": "$t(common.favorite)",
"albumArtist": "$t(entity.albumArtist_one)",
"rating": "$t(common.rating)",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"title": "$t(common.title)",
"size": "$t(common.size)",
"genre": "$t(entity.genre_one)",
"year": "$t(common.year)"
}
},
"column": {
"comment": "commentaire",
"album": "album",
"rating": "notation",
"favorite": "favoris",
"rating": "note",
"favorite": "favori",
"playCount": "lectures",
"releaseYear": "année",
"biography": "biographie",
@@ -515,7 +620,13 @@
"trackNumber": "piste",
"albumArtist": "artiste de l'album",
"path": "chemin",
"discNumber": "disque"
"discNumber": "disque",
"albumCount": "$t(entity.album_other)",
"lastPlayed": "dernière lecture",
"artist": "$t(entity.artist_one)",
"genre": "$t(entity.genre_one)",
"songCount": "$t(entity.track_other)",
"channels": "$t(common.channel_other)"
}
}
}
+43 -18
View File
@@ -29,7 +29,7 @@
"action_other": "azioni",
"biography": "biografia",
"bpm": "bpm",
"center": "centro",
"center": "centrale",
"cancel": "annulla",
"channel_one": "canale",
"channel_many": "canali",
@@ -46,7 +46,7 @@
"left": "sinistra",
"save": "salva",
"right": "destra",
"currentSong": "$t(entity.track_one) corrent",
"currentSong": "$t(entity.track_one) corrente",
"trackNumber": "traccia",
"descending": "decrescente",
"gap": "gap",
@@ -102,9 +102,9 @@
"note": "nota"
},
"player": {
"repeat_all": "ripeti tutto",
"repeat_all": "ripeti coda",
"stop": "ferma",
"repeat": "ripeti",
"repeat": "ripeti traccia",
"queue_remove": "rimuovi selezionati",
"playRandom": "riproduci casuale",
"skip": "salta",
@@ -113,21 +113,21 @@
"skip_back": "salta indietro",
"favorite": "preferito",
"next": "successivo",
"shuffle": "mischia",
"shuffle": "mescola",
"playbackFetchNoResults": "nessuna canzone trovata",
"playbackFetchInProgress": "caricamento canzoni…",
"addNext": "aggiungi successivo",
"playbackSpeed": "velocità riproduzione",
"playbackSpeed": "velocità di riproduzione",
"playbackFetchCancel": "ci sta mettendo un po'... chiudi la notifica per annullare",
"play": "riproduci",
"repeat_off": "ripeti disabilitato",
"repeat_off": "non ripetere",
"pause": "pausa",
"queue_clear": "cancella coda",
"muted": "silenziato",
"unfavorite": "togli dai preferiti",
"queue_moveToTop": "sposta selezionati in fondo",
"queue_moveToBottom": "sposta selezionati in cima",
"shuffle_off": "mischia disabilitato",
"shuffle_off": "non mescolare",
"addLast": "aggiungi in coda",
"mute": "silenzia",
"skip_forward": "salta avanti"
@@ -156,8 +156,8 @@
"crossfadeStyle": "stile dissolvenza",
"sidebarConfiguration": "configurazione barra laterale",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_zoomIn": "ingrandisci",
"scrobble_description": "esegui scrobble delle riproduzioni al tuo media server",
"hotkey_zoomIn": "ingrandisci layout",
"scrobble_description": "invia lo scrobble delle riproduzioni al tuo media server",
"audioExclusiveMode_description": "abilità modalità output esclusiva. In questa modalità il sistema è di solito chiuso fuori, e solo mpv potrà riprodurre audio",
"discordUpdateInterval": "intervallo aggiornamento stato attività {{discord}}",
"themeLight": "tema (chiaro)",
@@ -207,7 +207,7 @@
"crossfadeDuration_description": "imposta la durata dell'effetto di dissolvenza",
"language": "lingua",
"playbackStyle": "stile riproduzione",
"hotkey_toggleShuffle": "attiva/disattiva mischia",
"hotkey_toggleShuffle": "attiva/disattiva mescolamento",
"theme": "tema",
"playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio",
"discordRichPresence_description": "abilita lo status del playback nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}} ",
@@ -221,7 +221,7 @@
"enableRemote": "abilita controllo remoto server",
"savePlayQueue": "salva coda di riproduzione",
"minimumScrobbleSeconds_description": "la minima durata in secondi di una canzone che deve essere riprodutta prima di eseguire lo scrobble",
"fontType_description": "Font built-in selezionana uno dei font forniti da Feishin. Font di sistema ti permette di selezionare ogni fonti fornito dal tuo sistema operativo. Custom ti permette di fornire il tuo font",
"fontType_description": "Font built-in seleziona uno dei font forniti da Feishin. Font di sistema ti permette di selezionare ogni font fornito dal tuo sistema operativo. Custom ti permette di fornire il tuo font",
"playButtonBehavior": "comportamento pulsante riproduzione",
"volumeWheelStep": "step rotellina volume",
"sidebarPlaylistList_description": "mostra o nascondi la lista delle playlist nella barra laterale",
@@ -247,7 +247,7 @@
"crossfadeDuration": "durata dissolvenza",
"discordIdleStatus": "visualizza lo stato attività in stato inattivo",
"audioPlayer": "player audio",
"hotkey_zoomOut": "rimpicciolisci",
"hotkey_zoomOut": "rimpicciolisci layout",
"hotkey_rate0": "rimuovi voto",
"discordApplicationId": "application id {{discord}}",
"applicationHotkeys_description": "configura tasti a scelta rapida dell'applicazione. attiva/disattiva la casella per impostare un tasto a scelta rapida globale (solo desktop)",
@@ -265,13 +265,37 @@
"discordRichPresence": "stato attività {{discord}}",
"font_description": "imposta il font da usare per l'applicazione",
"savePlayQueue_description": "salva la coda di riproduzione quando l'applicazione viene chiusa e ripristina quando l'applicazione viene riaperta",
"useSystemTheme": "usa il tema di sistema"
"useSystemTheme": "usa il tema di sistema",
"replayGainMode_description": "aggiusta il volume secondo i valori {{ReplayGain}} salvati nei metadati del file",
"showSkipButtons": "mostra pulsanti per saltare",
"sampleRate": "frequenza di campionamento",
"sampleRate_description": "seleziona la frequenza di campionamento di output da usare se la frequenza di campionamento selezionata è diversa da quella della del media attuale",
"hotkey_togglePreviousSongFavorite": "imposta/rimuovi $t(common.previousSong) favorito",
"hotkey_unfavoritePreviousSong": "rimuovi $t(common.previousSong) dai preferiti",
"showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player",
"hotkey_unfavoriteCurrentSong": "rimuovi $t(common.currentSong) dai preferiti",
"hotkey_toggleCurrentSongFavorite": "imposta/rimuovi $t(common.currentSong) favorito",
"showSkipButton": "mostra pulsanti per saltare",
"hotkey_browserForward": "Vai avanti di una pagina",
"hotkey_browserBack": "Torna indietro di una pagina",
"sidebarCollapsedNavigation_description": "mostra o nascondi la navigazione nella barra laterale collassata",
"replayGainClipping_description": "Previeni il clipping causato da {{ReplayGain}} abbassando automaticamente il gain",
"replayGainPreamp": "preamplificazione {{ReplayGain}} (dB)",
"sidePlayQueueStyle": "stile della coda di riproduzione laterale",
"showSkipButtons_description": "mostra o nascondi i pulsanti per saltare dalla barra di riproduzione",
"skipPlaylistPage_description": "quando si naviga in una playlist, si va alla pagina dell'elenco dei brani della playlist invece che alla pagina predefinita",
"sidePlayQueueStyle_description": "imposta lo stile della coda di riproduzione laterale",
"replayGainMode": "modalità {{ReplayGain}}",
"replayGainFallback_description": "gain in db da applicare se il file non possiede tag {{ReplayGain}}",
"replayGainPreamp_description": "aggiusta la preamplificazione del gain applicato sui valori {{ReplayGain}}",
"skipPlaylistPage": "Salta la pagina playlist",
"sidebarCollapsedNavigation": "navigazione con barra laterale (collassata)"
},
"error": {
"remotePortWarning": "riavvia il server per applicare la nuova porta",
"systemFontError": "si è verificato un errore nell'otternere i font di sistema",
"playbackError": "si è verificato un errore nel provare a riprodurre il media",
"endpointNotImplementedError": "l'endpoint {{endpoint} is not implemented for {{serverType}}",
"endpointNotImplementedError": "l'endpoint {{endpoint}} non è implementato per {{serverType}}",
"remotePortError": "si è verificato un errore nel provare a impostare la porta del server remoto",
"serverRequired": "server richiesto",
"authenticationFailed": "autenticazione fallita",
@@ -368,7 +392,7 @@
"selectServer": "seleziona server",
"version": "versione {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "gestisci sever",
"manageServers": "gestisci server",
"expandSidebar": "espandi barra laterale",
"collapseSidebar": "collassa barra laterale",
"openBrowserDevtools": "apri devtools browser",
@@ -402,7 +426,7 @@
"recentlyPlayed": "riprodotti recentemente"
},
"albumDetail": {
"moreFromArtist": "di più da questo $t(entity.genre_one)",
"moreFromArtist": "di più da questo $t(entity.artist_one)",
"moreFromGeneric": "di più da {{item}}"
},
"setting": {
@@ -495,7 +519,8 @@
"size": "$t(common.size)"
},
"view": {
"table": "tabella"
"table": "tabella",
"card": "Scheda"
},
"label": {
"releaseDate": "data rilascio",
+15 -8
View File
@@ -34,7 +34,7 @@
"crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します",
"remotePort_description": "リモートコントロール サーバーのポートを設定します",
"hotkey_skipBackward": "前にスキップ",
"replayGainMode_description": "ファイルのメタデータに保存されている{{ReplayGain}}値に従って音量ゲインを調整します",
"replayGainMode_description": "ファイルのメタデータに保存されている {{ReplayGain}} 値に従って音量ゲインを調整します",
"volumeWheelStep_description": "音量スライダーでマウスホイールをスクロールしたときに変化する音量を設定します",
"audioDevice_description": "再生に使用するオーディオデバイスを選択します (Webプレーヤーのみ)",
"theme_description": "アプリケーションに使用するテーマを設定します",
@@ -54,18 +54,18 @@
"enableRemote_description": "リモートコントロール サーバーを有効化し、他のデバイスからアプリケーションを制御できるようにします",
"fontType_optionSystem": "システムフォント",
"mpvExecutablePath_description": "mpvの実行ファイルが存在するパスを設定します",
"replayGainClipping_description": "自動的にゲインを下げて{{ReplayGain}}によるクリッピングを防ぎます",
"replayGainClipping_description": "自動的にゲインを下げて {{ReplayGain}} によるクリッピングを防ぎます",
"replayGainPreamp": "{{ReplayGain}} プリアンプ (dB)",
"hotkey_favoriteCurrentSong": "$t(common.currentSong) をお気に入り",
"sampleRate": "サンプルレート",
"crossfadeStyle": "クロスフェードスタイル",
"sidePlayQueueStyle_optionAttached": "結合",
"sidebarConfiguration": "サイドバー設定",
"sampleRate_description": "サンプル周波数がメディア、選択とで異なる場合に使用する出力サンプルレートを選択します",
"sampleRate_description": "設定とメディアのサンプル周波数が異なる場合に使用する出力サンプルレートを選択します",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "{{ReplayGain}} クリッピング",
"hotkey_zoomIn": "拡大",
"scrobble_description": "メデアサーバーに再生をScrobbleさせます",
"scrobble_description": "再生した音楽をメデアサーバーから Scrobbleます",
"hotkey_browserForward": "ブラウザ 進む",
"audioExclusiveMode_description": "専用の排他出力モードを有効にします。 このモードでは、システムの他の出力がロックされ、mpvだけがオーディオを出力できるようになります",
"discordUpdateInterval": "{{discord}} Rich Presenceアップデート間隔",
@@ -136,7 +136,7 @@
"savePlayQueue": "再生キューを保存",
"minimumScrobbleSeconds_description": "Scrobbleされるために必要な最短の再生時間(秒)",
"skipPlaylistPage_description": "プレイリストに移動するときに、デフォルトページではなくプレイリストの曲リストページに移動します",
"fontType_description": "組み込みフォント、Feishin が提供するフォントから1つを選択します。 システムフォント、OSにインストール済みの任意のフォントを選択できます。 カスタムフォントは,\nフォントファイルを自身で選択できます",
"fontType_description": "組み込みフォントの場合、Feishin が提供するフォントから1つを選択します。 システムフォントの場合、OSにインストール済みの任意のフォントを選択できます。 カスタムフォントの場合、フォントファイルを自身で選択できます",
"playButtonBehavior": "再生ボタンの動作",
"volumeWheelStep": "音量ホイールステップ",
"sidebarPlaylistList_description": "サイドバーでプレイリストのリストを表示/非表示にします",
@@ -375,7 +375,8 @@
"mpvRequired": "MPVが必要です",
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
"invalidServer": "無効なサーバー",
"loginRateError": "ログイン試行回数が多すぎます、数秒後に再試行してください"
"loginRateError": "ログイン試行回数が多すぎます、数秒後に再試行してください",
"endpointNotImplementedError": "{{serverType}} にはエンドポイント {{endpoint}} が実装されていません"
},
"filter": {
"mostPlayed": "最も多く再生",
@@ -413,7 +414,13 @@
"trackNumber": "トラック",
"comment": "コメント",
"recentlyUpdated": "新規更新",
"isPublic": "共有済み"
"isPublic": "共有済み",
"channels": "$t(common.channel_other)",
"owner": "$t(common.owner)",
"genre": "$t(entity.genre_one)",
"albumCount": "$t(entity.album_other) 個",
"id": "id",
"album": "$t(entity.album_one)"
},
"page": {
"sidebar": {
@@ -485,7 +492,7 @@
"recentlyPlayed": "最近の再生"
},
"albumDetail": {
"moreFromArtist": "$t(entity.genre_one) の他の項目",
"moreFromArtist": "$t(entity.artist_one) の他の項目",
"moreFromGeneric": "{{item}} の他の項目"
},
"setting": {
+232
View File
@@ -0,0 +1,232 @@
{
"action": {
"editPlaylist": "pas $t(entity.playlist_one) aan",
"goToPage": "ga naar pagina",
"moveToTop": "verplaats naar top",
"addToFavorites": "toevoegen aan $t(entity.favorite_other)",
"addToPlaylist": "toevoegen aan $t(entity.playlist_one)",
"createPlaylist": "maak $t(entity.playlist_one)",
"removeFromPlaylist": "verwijder van $t(entity.playlist_one)",
"viewPlaylists": "bekijk $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "verwijder $t(entity.playlist_one)",
"removeFromQueue": "verwijder van lijst",
"deselectAll": "deselecteer alles",
"moveToBottom": "verplaats naar bodem",
"setRating": "selecteer rating",
"toggleSmartPlaylistEditor": "editor $t(entity.smartPlaylist) schakelen",
"removeFromFavorites": "verwijder van $t(entity.favorite_other)",
"clearQueue": "lijst leegmaken"
},
"common": {
"backward": "achteruit",
"increase": "verhogen",
"rating": "rating",
"bpm": "bpm",
"areYouSure": "weet je het zeker?",
"edit": "aanpassen",
"favorite": "favoriet",
"left": "links",
"currentSong": "huidig $t(entity.track_one)",
"collapse": "samenvouwen",
"descending": "aflopend",
"add": "toevoegen",
"gap": "gat",
"ascending": "oplopend",
"dismiss": "negeren",
"manage": "beheren",
"limit": "limiet",
"minimize": "minimaliseren",
"modified": "aangepast",
"duration": "duur",
"name": "naam",
"maximize": "maximaliseren",
"decrease": "verminder",
"ok": "ok",
"description": "beschrijving",
"configure": "configureren",
"path": "pad",
"center": "centreren",
"no": "nee",
"owner": "eigenaar",
"enable": "activeren",
"clear": "opschonen",
"forward": "vooruit",
"delete": "verwijder",
"cancel": "annuleer",
"forceRestartRequired": "herstart om aanpassingen toe te passen... wanneer de notificatie gesloten wordt zal de applicatie herstarten",
"filter_one": "filter",
"filter_other": "filters",
"filters": "filters",
"create": "aanmaken",
"bitrate": "bitrate",
"action_one": "actie",
"action_other": "acties",
"playerMustBePaused": "player moet gepauzeerd zijn",
"confirm": "bevestig",
"home": "home",
"comingSoon": "komt binnenkort…",
"channel_one": "kanaal",
"channel_other": "kanalen",
"disable": "deactiveren",
"none": "geen",
"menu": "menu",
"previousSong": "vorige $t(entity.track_one)",
"noResultsFromQuery": "de zoekopdracht leverde geen resultaten op",
"quit": "sluiten",
"expand": "vergroten",
"disc": "disk",
"random": "willekeurig",
"biography": "biografie",
"note": "Opmerking",
"refresh": "verversen",
"unknown": "onbekend",
"save": "opslaan",
"right": "rechts",
"trackNumber": "track",
"year": "jaar",
"version": "versie",
"title": "titel",
"saveAndReplace": "opslaan en vervangen",
"resetToDefault": "herstellen naar standaard",
"reset": "terugzetten",
"sortOrder": "volgorde",
"restartRequired": "herstart is nodig",
"search": "zoeken",
"saveAs": "opslaan als",
"yes": "ja",
"size": "grootte"
},
"filter": {
"rating": "rating",
"communityRating": "community rating",
"criticRating": "criticus rating",
"mostPlayed": "meest gespeeld",
"comment": "commentaar",
"playCount": "aantal keer afgespeeld",
"recentlyUpdated": "recentelijk geüpdate",
"channels": "$t(common.channel_other)",
"isCompilation": "is compilatie",
"recentlyPlayed": "recentelijk afgespeeld",
"isRated": "is rated",
"owner": "$t(common.owner)",
"bitrate": "bitrate",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "recentelijk toegevoegd",
"note": "notitie",
"name": "naam",
"dateAdded": "datum toegevoegd",
"albumCount": "$t(entity.album_other) totaal",
"path": "pad",
"favorited": "favoriet",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "is recentelijk afgespeeld",
"isFavorited": "is favoriet",
"bpm": "bpm",
"id": "id",
"disc": "disk",
"biography": "biografie",
"artist": "$t(entity.artist_one)",
"duration": "duratie",
"isPublic": "is publiek",
"random": "willekeurig",
"lastPlayed": "laatst gespeeld",
"fromYear": "van jaar",
"album": "$t(entity.album_one)",
"title": "titel",
"search": "zoeken",
"releaseDate": "releasedatum",
"releaseYear": "release jaar",
"songCount": "aantal nummers",
"toYear": "tot jaar",
"trackNumber": "track"
},
"page": {
"contextMenu": {
"setRating": "$t(action.setRating)"
}
},
"error": {
"remotePortWarning": "herstart de server om de nieuwe poort in te stellen",
"systemFontError": "er is iets fout gegaan tijdens het verkrijgen van systeem fonts",
"playbackError": "er is iets fout gegaan bij het afspelen van de media",
"endpointNotImplementedError": "endpoint {{endpoint}} is niet geïmplementeerd voor {{serverType}}",
"remotePortError": "er is iets fout gegaan tijdens het selecteren van de remote server",
"serverRequired": "server vereist",
"authenticationFailed": "authenticatie mislukt",
"apiRouteError": "verzoek kan niet doorgestuurd worden",
"genericError": "er is iets fout gegaan",
"credentialsRequired": "inloggegevens vereist",
"sessionExpiredError": "jouw sessie is verlopen",
"remoteEnableError": "er is iets fout gegaan tijdens het $t(common.enable) van de remote server",
"localFontAccessDenied": "toegang geweigerd tot lokale fonts",
"serverNotSelectedError": "geen server geselecteerd",
"remoteDisableError": "er is iets fout gegaan tijdens het $t(common.disable) van de remote server",
"mpvRequired": "MPV vereist",
"audioDeviceFetchError": "er is iets mis gegaan met het ophalen van de audioapparaten",
"invalidServer": "ongeldige server",
"loginRateError": "te veel login pogingen, probeer het opnieuw in een paar seconde"
},
"entity": {
"genre_one": "genre",
"genre_other": "genres",
"playlistWithCount_one": "{{count}} afspeellijst",
"playlistWithCount_other": "{{count}} afspeellijsten",
"playlist_one": "afspeellijst",
"playlist_other": "afspeellijsten",
"artist_one": "artiest",
"artist_other": "artiesten",
"folderWithCount_one": "{{count}} folder",
"folderWithCount_other": "{{count}} folders",
"albumArtist_one": "album artiest",
"albumArtist_other": "album artiesten",
"track_one": "track",
"track_other": "tracks",
"albumArtistCount_one": "{{count}} album artiest",
"albumArtistCount_other": "{{count}} album artiesten",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} albums",
"favorite_one": "favoriet",
"favorite_other": "favorieten",
"artistWithCount_one": "{{count}} artiest",
"artistWithCount_other": "{{count}} artiesten",
"folder_one": "folder",
"folder_other": "folders",
"smartPlaylist": "smart $t(entity.playlist_one)",
"album_one": "album",
"album_other": "albums",
"genreWithCount_one": "{{count}} genre",
"genreWithCount_other": "{{count}} genres",
"trackWithCount_one": "{{count}} track",
"trackWithCount_other": "{{count}} tracks"
},
"table": {
"column": {
"rating": "rating"
},
"config": {
"label": {
"rating": "$t(common.rating)"
}
}
},
"setting": {
"hotkey_rate5": "rating 5 sterren",
"hotkey_rate4": "rating 4 sterren"
},
"form": {
"addServer": {
"title": "server toevoegen",
"input_username": "gebruikersnaam",
"input_url": "url",
"input_password": "wachtwoord",
"input_legacyAuthentication": "activeer legacy authenticatie",
"input_name": "server naam",
"success": "server met succes toegevoegd",
"input_savePassword": "wachtwoord opslaan",
"ignoreSsl": "negeer ssl $t(common.restartRequired)",
"ignoreCors": "negeer cors $t(common.restartRequired)",
"error_savePassword": "er is iets mis gegaan met het opslaan van het wachtwoord"
}
}
}
+10 -10
View File
@@ -52,7 +52,7 @@
"delete": "usuń",
"cancel": "cofnij",
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
"setting": "ustawienie",
"setting": "ustawienia",
"version": "wersja",
"title": "tytuł",
"filter_one": "filtr",
@@ -237,8 +237,8 @@
"input_name": "nazwa serwera",
"success": "serwer dodany pomyślnie",
"input_savePassword": "zapisz hasło",
"ignoreSsl": "zignoruj ssl $t(common.restartRequired)",
"ignoreCors": "zignoruj cors $t(common.restartRequired)",
"ignoreSsl": "zignoruj ssl ($t(common.restartRequired))",
"ignoreCors": "zignoruj cors ($t(common.restartRequired))",
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła"
},
"addToPlaylist": {
@@ -314,7 +314,7 @@
"removeFromQueue": "$t(action.removeFromQueue)"
},
"albumDetail": {
"moreFromArtist": "więcej od $t(entity.genre_one)",
"moreFromArtist": "więcej od $t(entity.artist_one)",
"moreFromGeneric": "więcej od {{item}}"
},
"albumArtistList": {
@@ -370,7 +370,7 @@
"player": {
"repeat_all": "powtarzaj wszystkie",
"stop": "stop",
"repeat": "powtarzaj",
"repeat": "powtarzaj jeden",
"queue_remove": "usuń zaznaczone",
"playRandom": "odtwarzaj losowe",
"skip": "pomiń",
@@ -412,7 +412,7 @@
"crossfadeStyle": "styl przenikania",
"hotkey_zoomIn": "przybliż",
"hotkey_browserForward": "przeglądarka w przód",
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko pliki mpv",
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv",
"discordUpdateInterval": "{{discord}} interwał aktualizacji obszernej obecności",
"fontType_optionBuiltIn": "wbudowana czcionka",
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
@@ -438,7 +438,7 @@
"fontType_optionCustom": "czcionka niestandardowa",
"audioExclusiveMode": "wyłączny tryb audio",
"lyricFetchProvider": "dostawcy tekstów internetowych",
"language_description": "ustaw język dla aplikacji $t(common.restartRequired)",
"language_description": "ustaw język dla aplikacji ($t(common.restartRequired))",
"hotkey_rate3": "oceń na 3 gwiazdki",
"font": "czcionka",
"hotkey_toggleFullScreenPlayer": "przełącz tryb pełnoekranowy",
@@ -501,18 +501,18 @@
"mpvExecutablePath": "ścieżka pliku wykonywalnego mpv",
"playButtonBehavior_description": "ustaw domyślne zachowanie dla przycisku odtwarzania kiedy piosenka zostanie dodana do kolejki",
"minimumScrobblePercentage_description": "minimalny czas odtwarzania piosenki który musi upłynąć aby uznać ją za scrobble",
"minimumScrobbleSeconds_description": "minimalny czas odtwarzania piosenki w sekundach jaki musi upłynąć aby uznać ją za scrobble",
"minimumScrobbleSeconds_description": "minimalny czas odtwarzania piosenki w sekundach jaki musi upłynąć aby uznać ją za scrobbling",
"playButtonBehavior": "zachowanie przycisku odtwarzania",
"playbackStyle_optionNormal": "normalny",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"minimumScrobbleSeconds": "minimalne scrobble (sekund)",
"minimumScrobbleSeconds": "minimalne scrobble (w sekundach)",
"remotePort_description": "ustaw port dla serwera zdalnej kontroli",
"replayGainMode_description": "dostosuj wzmocnienie dźwięku zgodnie z wartościami {{ReplayGain}} przechowywanymi w metadanych do pliku",
"replayGainFallback": "rezerwowy {{ReplayGain}}",
"sidebarCollapsedNavigation_description": "pokaż lub ukryj nawigację na zwiniętym pasku bocznym",
"skipDuration": "czas trwania pominięcia",
"showSkipButtons": "pokaż przyciski pomijania",
"scrobble": "scrobble",
"scrobble": "scrobbling",
"skipDuration_description": "ustaw czas pominięcia kiedy zostanie użyty przycisk pominięcia na pasku odtwarzania",
"replayGainClipping_description": "Zapobiegaj wzmocnieniu spowodowanemu przez {{ReplayGain}} na automatyczne obniżanie wzmocnienia",
"replayGainPreamp": "przedwzmacniacz {{ReplayGain}} (db)",
+224 -3
View File
@@ -9,8 +9,78 @@
"bitrate": "taxa de bits",
"action_one": "ação",
"action_many": "ações",
"action_other": "(n == 0 || n == 1) ? ação : ações",
"biography": "biografia"
"action_other": "ações",
"biography": "biografia",
"bpm": "bpm",
"edit": "editar",
"favorite": "favorito",
"currentSong": "$t(entity.track_one) atual",
"descending": "abaixar",
"dismiss": "liberar",
"duration": "duração",
"decrease": "diminuir",
"description": "descrição",
"configure": "configurar",
"enable": "habilitar",
"clear": "limpar",
"delete": "deletar",
"title": "titulo",
"create": "criar",
"confirm": "confirmar",
"home": "inicio",
"comingSoon": "em breve…",
"channel_one": "canal",
"channel_many": "canais",
"channel_other": "canais",
"disable": "desabilitar",
"expand": "expandir",
"disc": "disco",
"increase": "incrementar",
"rating": "classificação",
"refresh": "atualizar",
"unknown": "desconhecido",
"left": "esquerda",
"save": "salvar",
"right": "direita",
"collapse": "minimizar",
"trackNumber": "faixa",
"gap": "intervalo",
"year": "ano",
"manage": "gerenciar",
"limit": "limite",
"minimize": "minimizar",
"modified": "modificado",
"name": "nome",
"maximize": "maximizar",
"ok": "ok",
"path": "caminho",
"no": "não",
"owner": "dono",
"forward": "avançar",
"forceRestartRequired": "reinicie para aplicar as alterações… feche a notificação para reiniciar",
"setting": "contexto",
"version": "versão",
"filter_one": "filtro",
"filter_many": "filtros",
"filter_other": "filtros",
"filters": "filtros",
"saveAndReplace": "salvar e substituir",
"playerMustBePaused": "o player deve estar pausado",
"resetToDefault": "restaurar ao padrão",
"reset": "reiniciar",
"sortOrder": "ordem",
"none": "nenhum",
"menu": "menu",
"restartRequired": "é necessário reiniciar",
"previousSong": "anterior $t(entity.track_one)",
"noResultsFromQuery": "a consulta não retornou resultados",
"quit": "abandonar",
"search": "procurar",
"saveAs": "salvar como",
"yes": "sim",
"random": "aleatório",
"size": "tamanho",
"note": "observação"
},
"action": {
"goToPage": "vá para página",
@@ -20,6 +90,157 @@
"moveToTop": "mover para o topo",
"refresh": "$t(common.refresh)",
"removeFromQueue": "remover da fila",
"moveToBottom": "mover para baixo"
"moveToBottom": "mover para baixo",
"editPlaylist": "editar $t(entity.playlist_one)",
"clearQueue": "limpar fila",
"addToPlaylist": "adicionar à $t(entity.playlist_one)",
"createPlaylist": "criar $t(entity.playlist_one)",
"removeFromPlaylist": "remover da $t(entity.playlist_one)",
"deletePlaylist": "deletar $t(entity.playlist_one)",
"deselectAll": "desmarcar todos",
"removeFromFavorites": "remover de $t(entity.favorite_other)"
},
"form": {
"deletePlaylist": {
"title": "deletar $t(entity.playlist_one)"
},
"addServer": {
"title": "adicionar servidor"
},
"createPlaylist": {
"title": "criar $t(entity.playlist_one)"
},
"updateServer": {
"title": "atualizar servidor"
},
"editPlaylist": {
"title": "editar $t(entity.playlist_one)"
},
"addToPlaylist": {
"title": "adicionar à $t(entity.playlist_one)"
},
"lyricSearch": {
"title": "pesquisa de letras"
}
},
"setting": {
"discordIdleStatus_description": "quando ativado, atualiza o status enquanto o player está ocioso",
"discordUpdateInterval_description": "o tempo em segundos entre cada atualização (mínimo 15 segundos)",
"playButtonBehavior_description": "define o comportamento padrão do botão play ao adicionar músicas à fila",
"discordApplicationId": "{{discord}} ID do aplicativo"
},
"table": {
"config": {
"label": {
"title": "$t(common.title)",
"titleCombined": "$t(common.title) (combinado)",
"discNumber": "numero do disco"
}
},
"column": {
"title": "titulo",
"discNumber": "disco"
}
},
"page": {
"home": {
"mostPlayed": "mais tocado",
"newlyAdded": "lançamentos recém-adicionados",
"title": "$t(common.home)",
"explore": "explore a sua biblioteca",
"recentlyPlayed": "tocado recentemente"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"globalSearch": {
"title": "comandos"
},
"sidebar": {
"home": "$t(common.home)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"filter": {
"title": "titulo",
"disc": "disco",
"mostPlayed": "mais tocado"
},
"player": {
"playbackFetchNoResults": "nenhuma música encontrada",
"playbackFetchInProgress": "carregando músicas…"
},
"entity": {
"albumArtist_one": "artista do álbum",
"albumArtist_many": "artistas do álbum",
"albumArtist_other": "artistas do álbum",
"albumArtistCount_one": "{{count}} artista do álbum",
"albumArtistCount_many": "{{count}} artistas do álbum",
"albumArtistCount_other": "{{count}} artistas do álbum",
"album_one": "álbum",
"album_many": "álbuns",
"album_other": "álbuns",
"artist_one": "artista",
"artist_many": "artistas",
"artist_other": "artistas",
"albumWithCount_one": "{{count}} álbum",
"albumWithCount_many": "{{count}} álbuns",
"albumWithCount_other": "{{count}} álbuns",
"favorite_one": "favorito",
"favorite_many": "favoritos",
"favorite_other": "favoritos",
"artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistas",
"artistWithCount_other": "{{count}} artistas",
"folder_one": "pasta",
"folder_many": "pastas",
"folder_other": "pastas",
"genre_one": "gênero",
"genre_many": "gêneros",
"genre_other": "gêneros",
"playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_many": "{{count}} playlists",
"playlistWithCount_other": "{{count}} playlists",
"playlist_one": "playlist",
"playlist_many": "playlists",
"playlist_other": "playlists",
"folderWithCount_one": "{{count}} pasta",
"folderWithCount_many": "{{count}} pastas",
"folderWithCount_other": "{{count}} pastas",
"genreWithCount_one": "{{count}} gênero",
"genreWithCount_many": "{{count}} gêneros",
"genreWithCount_other": "{{count}} gêneros"
},
"error": {
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
"systemFontError": "ocorreu um erro ao tentar obter fontes do sistema",
"playbackError": "ocorreu um erro ao tentar reproduzir a mídia",
"endpointNotImplementedError": "endpoint {{endpoint}} não está implementado para {{serverType}}",
"remotePortError": "ocorreu um erro ao tentar definir a porta do servidor remoto",
"serverRequired": "servidor necessário",
"authenticationFailed": "falha na autenticação",
"apiRouteError": "não é possível encaminhar a solicitação",
"genericError": "um erro ocorreu",
"credentialsRequired": "credenciais necessárias",
"sessionExpiredError": "sua sessão expirou",
"remoteEnableError": "ocorreu um erro ao tentar $t(common.enable) o servidor remoto",
"localFontAccessDenied": "acesso negado a fontes locais",
"serverNotSelectedError": "nenhum servidor selecionado",
"remoteDisableError": "ocorreu um erro ao tentar $t(common.disable) o servidor remoto",
"mpvRequired": "MPV necessário",
"audioDeviceFetchError": "ocorreu um erro ao tentar obter dispositivos de áudio",
"invalidServer": "servidor inválido",
"loginRateError": "muitas tentativas de login, tente novamente em alguns segundos"
}
}
+244 -4
View File
@@ -77,7 +77,7 @@
"playerMustBePaused": "воспроизведение должно быть остановлено",
"confirm": "подтвердить",
"resetToDefault": "сбросить к настройкам по умолчанию",
"home": "домой",
"home": "Главная страница",
"comingSoon": "скоро будет…",
"reset": "сбросить",
"channel_one": "канал",
@@ -91,7 +91,7 @@
"noResultsFromQuery": "нет результатов",
"quit": "выйти",
"expand": "расширить",
"search": "поиск",
"search": "Поиск",
"saveAs": "сохранить как",
"disc": "диск",
"yes": "да",
@@ -211,7 +211,7 @@
"remotePortWarning": "перезапустить сервер для применения нового порта",
"systemFontError": "произошла ошибка при попытке получить системные шрифты",
"playbackError": "произошла ошибка при попытке проиграть медиа",
"endpointNotImplementedError": "запрос {{endpoint} is not implemented for {{serverType}}",
"endpointNotImplementedError": "запрос {{endpoint}} is not implemented for {{serverType}}",
"remotePortError": "произошла ошибка при попытке установить порт удаленного сервера",
"serverRequired": "необходим сервер",
"authenticationFailed": "аутентификация завершилась с ошибкой",
@@ -243,6 +243,246 @@
"artist": "$t(entity.artist_one)",
"duration": "продолжительность",
"fromYear": "из года",
"criticRating": "рейтинг критиков"
"criticRating": "рейтинг критиков",
"mostPlayed": "наибольшое кол-во воспроизведений",
"comment": "комментировать",
"playCount": "кол-во воспроизведений",
"recentlyUpdated": "недавно обновлено",
"channels": "$t(common.channel_other)",
"recentlyPlayed": "недавно проиграно",
"owner": "$t(common.owner)",
"title": "название",
"rating": "рейтинг",
"search": "Поиск",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "недавно добавлено",
"note": "заметка",
"name": "название",
"releaseDate": "дата выхода",
"albumCount": "$t(entity.album_other) кол-во",
"path": "путь",
"isRecentlyPlayed": "недавно проигрывалась",
"releaseYear": "год выхода",
"id": "#",
"songCount": "кол-во песен",
"isPublic": "публичный",
"random": "случайный",
"lastPlayed": "последний раз проигрывалась",
"toYear": "до года",
"album": "$t(entity.album_one)",
"trackNumber": "трек"
},
"player": {
"repeat_all": "повтор всех",
"stop": "остановить",
"repeat": "повтор",
"queue_remove": "удалить выделенные",
"playRandom": "случайные песни",
"skip": "пропустить",
"previous": "предыдущий",
"toggleFullscreenPlayer": "включить полноэкранный режим",
"skip_back": "назад",
"favorite": "любимый",
"next": "следующее",
"shuffle": "перемешать",
"playbackFetchNoResults": "нет песен",
"playbackFetchInProgress": "загрузка песен..",
"addNext": "добавить следующий",
"playbackSpeed": "скорость воспроизведения",
"playbackFetchCancel": "это занимает некоторое время... закрыть уведомление для отмены",
"play": "прослушать",
"repeat_off": "повтор выключен",
"pause": "пауза",
"queue_clear": "очистить очередь",
"muted": "звук отключён",
"unfavorite": "убрать из любимых",
"queue_moveToTop": "переместить выделение вниз",
"queue_moveToBottom": "переместить выделение вверх",
"shuffle_off": "перемешивание выключено",
"addLast": "добавить последний",
"mute": "отключить звук",
"skip_forward": "вперёд"
},
"page": {
"sidebar": {
"nowPlaying": "Cейчас проигрывается",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
},
"fullscreenPlayer": {
"config": {
"showLyricMatch": "показать слова песни",
"dynamicBackground": "динамический фон",
"synchronized": "синхронизировано",
"followCurrentLyric": "следовать за текущими словами песни",
"opacity": "непрозрачность",
"lyricSize": "размер слов",
"showLyricProvider": "показать провайдера слов",
"unsynchronized": "несинхронизировано",
"lyricAlignment": "выравнивание слов песни",
"useImageAspectRatio": "использовать соотношение сторон изображения",
"lyricGap": "пробел между словами"
},
"upNext": "следующее",
"lyrics": "слова песни",
"related": "схожие"
},
"appMenu": {
"selectServer": "выбрать сервер",
"version": "версия {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "настроить список серверов",
"expandSidebar": "развернуть",
"collapseSidebar": "Скрыть боковую панель",
"openBrowserDevtools": "открыть инструменты разработчика",
"quit": "$t(common.quit)",
"goBack": "назад",
"goForward": "вперёд"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} выбрано",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"home": {
"mostPlayed": "наибольшее кол-во воспроизведений",
"newlyAdded": "недавно добавленные релизы",
"title": "$t(common.home)",
"explore": "изучите вашу медиатеку",
"recentlyPlayed": "недавно прослушано"
},
"albumDetail": {
"moreFromArtist": "больше из жанра $t(entity.genre_one)",
"moreFromGeneric": "больше из {{item}}"
},
"setting": {
"playbackTab": "воспроизведение",
"generalTab": "общее",
"hotkeysTab": "горячие клавиши",
"windowTab": "окно"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"globalSearch": {
"commands": {
"serverCommands": "команды сервера",
"goToPage": "перейти на страницу",
"searchFor": "поиск {{query}}"
},
"title": "комманды"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"form": {
"deletePlaylist": {
"title": "удалить $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) успешно удалён",
"input_confirm": "напишите название $t(entity.playlist_one), чтобы подтвердить действие"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "создать $t(entity.playlist_one)",
"input_public": "публичный",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) успешно создан",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "добавить сервер",
"input_username": "пользователь",
"input_url": "url",
"input_password": "пароль",
"input_legacyAuthentication": "включить старую аутентификацию",
"input_name": "название сервера",
"success": "сервер добавлен успешно",
"input_savePassword": "сохранить пароль",
"ignoreSsl": "ignore ssl $t(common.restartRequired)",
"ignoreCors": "$t(common.restartRequired)",
"error_savePassword": "произошла ошибка во время попытки сохранения пароля"
},
"addToPlaylist": {
"success": "добавлено(а) {{message}} $t(entity.song_other) в {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "добавить в $t(entity.playlist_one)",
"input_skipDuplicates": "пропустить дубликаты",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "обновить сервер",
"success": "сервер успешно обновлён"
},
"queryEditor": {
"input_optionMatchAll": "сопоставить все",
"input_optionMatchAny": "сопоставить любой"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "поиск слов песни"
},
"editPlaylist": {
"title": "редактировать $t(entity.playlist_one)"
}
},
"setting": {
"accentColor": "цвет акцента",
"accentColor_description": "устанавливает цвет акцента для приложения",
"applicationHotkeys": "горячие клавиши приложения",
"crossfadeStyle_description": "Выберите вид эффекта crossfade для аудиоплеера",
"enableRemote_description": "Включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
"fontType_optionSystem": "Системный шрифт",
"mpvExecutablePath_description": "Укажите папку, в которой находится исполняющий файл аудиоплеера MPV",
"crossfadeStyle": "Вид эффекта crossfade",
"fontType_optionBuiltIn": "Встроенный в приложение",
"disableLibraryUpdateOnStartup": "Отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "Сворачивать приложение в панель уведомлений",
"audioPlayer_description": "Укажите - какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "Отключить проверку обновлений",
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
"fontType_optionCustom": "Пользовательский шрифт",
"remotePassword": "Пароль к серверу удалённого управления",
"font": "Шрифт",
"crossfadeDuration_description": "Укажите длительность эффекта crossfade",
"mpvExecutablePath": "Папка с аудиоплеером MPV",
"exitToTray": "Сворачивать в панель уведомлений при закрытии",
"enableRemote": "Включить сервер удалённого управления",
"fontType": "Источник шрифта",
"crossfadeDuration": "Длительность эффекта crossfade",
"audioPlayer": "Аудиоплеер",
"minimizeToTray": "Сворачивать в панель уведомлений",
"font_description": "Выберите - какой шрифт использовать в приложении",
"remoteUsername": "Имя пользователя для доступа к серверу удалённого управления"
}
}
+632
View File
@@ -0,0 +1,632 @@
{
"player": {
"repeat_all": "ponavljaj sve",
"stop": "zaustavi",
"repeat": "ponavljaj jednu",
"queue_remove": "ukloni izabrane",
"playRandom": "slučajna reprodukcija",
"skip": "preskoči",
"previous": "prethodna",
"toggleFullscreenPlayer": "prebaci u puni ekran",
"skip_back": "preskoči unazad",
"favorite": "omiljeno",
"next": "sledeća",
"shuffle": "mešaj",
"playbackFetchNoResults": "nema pronađenih pesama",
"playbackFetchInProgress": "učitavanje pesama…",
"addNext": "dodaj sledeći",
"playbackSpeed": "brzina reprodukcije",
"playbackFetchCancel": "ovo traje... zatvorite obaveštenje da biste otkazali",
"play": "pusti",
"repeat_off": "ponavljanje isključeno",
"pause": "pauziraj",
"queue_clear": "isprazni red",
"muted": "isključeno",
"unfavorite": "ukloni iz omiljenih",
"queue_moveToTop": "pomeri izabrane na dno",
"queue_moveToBottom": "pomeri izabrane na vrh",
"shuffle_off": "mešanje isključeno",
"addLast": "dodaj poslednji",
"mute": "isključi ton",
"skip_forward": "preskoči unapred"
},
"setting": {
"crossfadeStyle_description": "izaberite stil prelaska za audio plejer",
"remotePort_description": "postavlja port za daljinsku kontrolu servera",
"hotkey_skipBackward": "preskoči unazad",
"replayGainMode_description": "prilagođava jačinu glasnoće prema vrednostima {{ReplayGain}} koje se nalaze u metapodacima datoteke",
"volumeWheelStep_description": "količina promene glasnoće pri okretanju točkića miša na traci za glasnoću",
"audioDevice_description": "izaberite audio uređaj za reprodukciju (samo veb plejer)",
"theme_description": "postavlja temu za aplikaciju",
"hotkey_playbackPause": "pauza",
"replayGainFallback": "{{ReplayGain}} alternativa",
"sidebarCollapsedNavigation_description": "prikaži ili sakrij navigaciju u sklopljenoj bočnoj traci",
"mpvExecutablePath_help": "po jedna po liniji",
"hotkey_volumeUp": "pojačaj glasnoću",
"skipDuration": "dužina preskakanja",
"discordIdleStatus_description": "kada je omogućeno, ažurira status dok je plejer u mirovanju",
"showSkipButtons": "prikaži dugmad za preskakanje",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "minimum trajanja za bilježenje (u procentima)",
"lyricFetch": "preuzimanje tekstova sa interneta",
"scrobble": "bilježi",
"skipDuration_description": "postavlja dužinu preskakanja kada koristite dugmad za preskakanje na traci za reprodukciju",
"enableRemote_description": "omogućava daljinsku kontrolu servera kako bi omogućili drugim uređajima da kontrolišu aplikaciju",
"fontType_optionSystem": "sistemski font",
"mpvExecutablePath_description": "postavlja putanju do izvršne datoteke mpv player-a",
"replayGainClipping_description": "Smanjuje preklapanje uzrokovano {{ReplayGain}} automatskim smanjenjem glasnoće",
"replayGainPreamp": "{{ReplayGain}} pojačalo (dB)",
"hotkey_favoriteCurrentSong": "omiljena $t(common.currentSong)",
"sampleRate": "sample rate",
"crossfadeStyle": "stil prelaza",
"sidePlayQueueStyle_optionAttached": "priložena",
"sidebarConfiguration": "konfiguracija bočne trake",
"sampleRate_description": "izaberite izlazni sample rate koji će se koristiti ako je sample rate drugačiji od onog u trenutnom mediju",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "{{ReplayGain}} smanjenje",
"hotkey_zoomIn": "uvećaj",
"scrobble_description": "bilježi reprodukciju na vašem serverskom uređaju",
"hotkey_browserForward": "napred u pregledaču",
"audioExclusiveMode_description": "omogućava ekskluzivan režim izlaza. U ovom režimu, sistem je obično zaključan, i samo mpv će moći da izlazi zvuk",
"discordUpdateInterval": "{{discord}} interval ažuriranja bogatog prikaza",
"themeLight": "tema (svetla)",
"fontType_optionBuiltIn": "ugrađeni font",
"hotkey_playbackPlayPause": "reprodukcija / pauza",
"hotkey_rate1": "oceni sa 1 zvezdicom",
"hotkey_skipForward": "preskoči unapred",
"disableLibraryUpdateOnStartup": "onemogući proveru za nove verzije pri pokretanju",
"discordApplicationId_description": "ID aplikacije za {{discord}} bogat prikaz (podrazumevano je {{defaultId}})",
"sidePlayQueueStyle": "stil bočne liste za reprodukciju",
"gaplessAudio": "bez pauze zvuka",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"zoom": "stepen zumiranja",
"minimizeToTray_description": "minimizira aplikaciju u sistemsku traku kada se zatvori i vraća je kada se ponovo otvori",
"hotkey_playbackPlay": "pusti",
"hotkey_togglePreviousSongFavorite": "promeni omiljenu pesmu $t(common.previousSong)",
"hotkey_volumeDown": "smanji glasnoću",
"hotkey_unfavoritePreviousSong": "ukloni omiljenu pesmu $t(common.previousSong)",
"audioPlayer_description": "izaberite audio plejer za reprodukciju",
"globalMediaHotkeys": "globalni medijski tasteri",
"hotkey_globalSearch": "globalno pretraživanje",
"gaplessAudio_description": "postavlja opciju bez pauze zvuka za mpv (preporučeno: slabo)",
"remoteUsername_description": "postavlja korisničko ime za daljinsku kontrolu servera. Ako su i korisničko ime i lozinka prazni, autentifikacija će biti onemogućena",
"disableAutomaticUpdates": "onemogući automatsko ažuriranje",
"exitToTray_description": "izlazak aplikacije u sistemsku traku",
"followLyric_description": "pomera tekst pesme na trenutnu poziciju reprodukcije",
"hotkey_favoritePreviousSong": "omiljena $t(common.previousSong)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"lyricOffset": "pomeraj teksta (ms)",
"discordUpdateInterval_description": "vreme u sekundama između svakog ažuriranja (minimum 15 sekundi)",
"fontType_optionCustom": "prilagođeni font",
"themeDark_description": "postavlja tamnu temu za aplikaciju",
"audioExclusiveMode": "ekskluzivni audio režim",
"remotePassword": "lozinka za daljinsku kontrolu servera",
"lyricFetchProvider": "pružatelji tekstova za preuzimanje",
"language_description": "postavlja jezik za aplikaciju ($t(common.restartRequired))",
"playbackStyle_optionCrossFade": "prelazak sa preklapanjem",
"hotkey_rate3": "oceni sa 3 zvezdice",
"font": "font",
"mpvExtraParameters": "mpv parametri",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "postavlja svetlu temu za aplikaciju",
"hotkey_toggleFullScreenPlayer": "prebaci na prikaz na celom ekranu",
"hotkey_localSearch": "pretraživanje na stranici",
"hotkey_toggleQueue": "promeni listu za reprodukciju",
"zoom_description": "postavlja stepen zumiranja za aplikaciju",
"remotePassword_description": "postavlja lozinku za daljinsku kontrolu servera. Ove informacije se prenose nezaštićeno, pa biste trebali koristiti jedinstvenu lozinku koja vam nije važna.",
"hotkey_rate5": "oceni sa 5 zvezdica",
"hotkey_playbackPrevious": "prethodna pesma",
"showSkipButtons_description": "prikaži ili sakrij dugmad za preskakanje na traci za reprodukciju",
"crossfadeDuration_description": "postavi trajanje efekta prelaza",
"language": "jezik",
"playbackStyle": "stil reprodukcije",
"hotkey_toggleShuffle": "promeni slučajan redosled",
"theme": "tema",
"playbackStyle_description": "izaberite stil reprodukcije za audio plejer",
"discordRichPresence_description": "omogućava status reprodukcije u {{discord}} bogatom prikazu. Ključevi slika su: {{icon}}, {{playing}}, i {{paused}} ",
"mpvExecutablePath": "putanja do mpv izvršne datoteke",
"audioDevice": "audio uređaj",
"hotkey_rate2": "oceni sa 2 zvezdice",
"playButtonBehavior_description": "postavlja zadano ponašanje dugmeta za reprodukciju pri dodavanju pesama u listu za reprodukciju",
"minimumScrobblePercentage_description": "minimalni procenat pesme koji mora da bude reprodukovan pre nego što se zabeleži",
"exitToTray": "izlazak u oblast za traku",
"hotkey_rate4": "oceni sa 4 zvezdice",
"enableRemote": "omogući daljinsku kontrolu servera",
"showSkipButton_description": "prikaži ili sakrij dugmad za preskakanje na traci za reprodukciju",
"savePlayQueue": "sačuvaj listu za reprodukciju",
"minimumScrobbleSeconds_description": "minimalno trajanje pesme u sekundama koje mora biti reprodukovano pre nego što se zabeleži",
"skipPlaylistPage_description": "kada idete na plejlistu, idi na stranicu sa pesmama plejliste umesto na podrazumevanu stranicu",
"fontType_description": "ugrađeni font bira jedan od fontova koje pruža Feishin. sistemski font vam omogućava da izaberete bilo koji font koji nudi vaš operativni sistem. prilagođeni vam omogućava da koristite svoj font",
"playButtonBehavior": "ponašanje dugmeta za reprodukciju",
"volumeWheelStep": "korak točkića za glasnoću",
"sidebarPlaylistList_description": "prikaži ili sakrij listu plejlista na bočnoj traci",
"accentColor": "akcentna boja",
"sidePlayQueueStyle_description": "postavlja stil bočne liste za reprodukciju",
"accentColor_description": "postavi akcentnu boju za aplikaciju",
"replayGainMode": "{{ReplayGain}} režim",
"playbackStyle_optionNormal": "normalno",
"windowBarStyle": "stil trake prozora",
"floatingQueueArea": "prikaži područje plutajuće liste za reprodukciju",
"replayGainFallback_description": "jačina u dB koja će se primeniti ako datoteka nema {{ReplayGain}} oznake",
"replayGainPreamp_description": "prilagođava pojačalo za {{ReplayGain}} vrednosti",
"hotkey_toggleRepeat": "promeni ponavljanje",
"lyricOffset_description": "pomera tekst za navedeni broj milisekundi",
"sidebarConfiguration_description": "izaberite stavke i redosled u kojem se pojavljuju u bočnoj traci",
"fontType": "tip fonta",
"remotePort": "port za daljinsku kontrolu servera",
"applicationHotkeys": "prečice za aplikaciju",
"hotkey_playbackNext": "sledeća pesma",
"useSystemTheme_description": "prati sistemski određene postavke za svetlu ili tamnu temu",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "preuzimanje tekstova sa različitih izvora na internetu",
"lyricFetchProvider_description": "izaberite pružatelje tekstova za preuzimanje. Redosled pružatelja je redosled upita.",
"globalMediaHotkeys_description": "omogućava ili onemogućava korišćenje medijskih tastera sistema za kontrolu reprodukcije",
"customFontPath": "prilagođena putanja fonta",
"followLyric": "prati trenutni tekst pesme",
"crossfadeDuration": "trajanje prelaza",
"discordIdleStatus": "prikaži status u mirovanju na Diskordu",
"sidePlayQueueStyle_optionDetached": "odvojena",
"audioPlayer": "audio plejer",
"hotkey_zoomOut": "umanji",
"hotkey_unfavoriteCurrentSong": "ukloni omiljenu pesmu $t(common.currentSong)",
"hotkey_rate0": "obrisati ocenu",
"discordApplicationId": "{{discord}} ID aplikacije",
"applicationHotkeys_description": "konfiguriši prečice za aplikaciju. uključite opciju za postavljanje kao globalne prečice (samo na radnoj površini)",
"floatingQueueArea_description": "prikaz ikone na desnoj strani ekrana za pregled liste za reprodukciju",
"hotkey_volumeMute": "isključi zvuk",
"hotkey_toggleCurrentSongFavorite": "promeni omiljenu pesmu $t(common.currentSong)",
"remoteUsername": "korisničko ime za daljinsku kontrolu servera",
"hotkey_browserBack": "nazad u pregledaču",
"showSkipButton": "prikaži dugmad za preskakanje",
"sidebarPlaylistList": "lista plejlista na bočnoj traci",
"minimizeToTray": "minimiziraj u sistemsku traku",
"skipPlaylistPage": "preskoči stranicu plejliste",
"themeDark": "tema (tamna)",
"sidebarCollapsedNavigation": "navigacija (skupljena bočna traka)",
"customFontPath_description": "postavlja putanju do prilagođenog fonta za aplikaciju",
"gaplessAudio_optionWeak": "slabo (preporučeno)",
"minimumScrobbleSeconds": "minimalno trajanje za bilježenje (u sekundama)",
"hotkey_playbackStop": "zaustavi",
"windowBarStyle_description": "izaberite stil trake prozora",
"discordRichPresence": "{{discord}} bogat prikaz",
"font_description": "postavlja font koji se koristi za aplikaciju",
"savePlayQueue_description": "sačuva listu za reprodukciju kada se aplikacija zatvori i obnovi je pri ponovnom otvaranju aplikacije",
"useSystemTheme": "koristi sistemsku temu"
},
"action": {
"editPlaylist": "izmeni $t(entity.playlist_one)",
"goToPage": "idi na stranu",
"moveToTop": "idi na vrh",
"clearQueue": "očisti listu",
"addToFavorites": "dodaj u $t(entity.favorite_other)",
"addToPlaylist": "dodaj u $t(entity.playlist_one)",
"createPlaylist": "napravi $t(entity.playlist_one)",
"removeFromPlaylist": "ukloni iz $t(entity.playlist_one)",
"viewPlaylists": "vidi $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "obriši $t(entity.playlist_one)",
"removeFromQueue": "ukloni iz liste",
"deselectAll": "deselektuj sve",
"moveToBottom": "idi na dno",
"setRating": "oceni",
"toggleSmartPlaylistEditor": "pokreni $t(entity.smartPlaylist) editor",
"removeFromFavorites": "ukloni iz $t(entity.favorite_other)"
},
"common": {
"backward": "nazad",
"increase": "povećaj",
"rating": "ocena",
"bpm": "bpm",
"refresh": "osveži",
"unknown": "nepoznato",
"areYouSure": "da li si siguran/na?",
"edit": "izmeni",
"favorite": "favorit",
"left": "levo",
"save": "sačuvaj",
"right": "desno",
"currentSong": "trenutno $t(entity.track_one)",
"collapse": "sklopi",
"trackNumber": "pesma",
"descending": "silazno",
"add": "dodaj",
"gap": "procep",
"ascending": "uzlazno",
"dismiss": "odbaci",
"year": "godina",
"manage": "upravljaj",
"limit": "limit",
"minimize": "minimiziraj",
"modified": "modifikovan",
"duration": "trajanje",
"name": "ime",
"maximize": "maksimiziraj",
"decrease": "smanji",
"ok": "ok",
"description": "opis",
"configure": "konfiguriši",
"path": "putanja",
"center": "centar",
"no": "ne",
"owner": "vlasnik",
"enable": "uključi",
"clear": "očisti",
"forward": "napred",
"delete": "obriši",
"cancel": "otkaži",
"forceRestartRequired": "restartuj da primeniš izmene… zatvori notifikaciju za restart",
"setting": "podešavanje",
"version": "verzija",
"title": "naziv",
"filter_one": "filter",
"filter_few": "filteri",
"filter_other": "filtera",
"filters": "filteri",
"create": "napravi",
"bitrate": "bitrejt",
"saveAndReplace": "sačuvaj i zameni",
"action_one": "akcija",
"action_few": "akcije",
"action_other": "akcija",
"playerMustBePaused": "plejer mora biti pauziran",
"confirm": "potvrdi",
"resetToDefault": "reset na fabrička podešavanja",
"home": "kuća",
"comingSoon": "stiže uskoro…",
"reset": "reset",
"channel_one": "kanal",
"channel_few": "kanali",
"channel_other": "kanala",
"disable": "onemogući",
"sortOrder": "redosled",
"none": "nijedan",
"menu": "meni",
"restartRequired": "restart potreban",
"previousSong": "prethodna $t(entity.track_one)",
"noResultsFromQuery": "upit je bez rezultata",
"quit": "izađi",
"expand": "proširi",
"search": "pretraga",
"saveAs": "sačuvaj kao",
"disc": "disk",
"yes": "da",
"random": "nasumično",
"size": "veličina",
"biography": "biografija",
"note": "notacija"
},
"table": {
"config": {
"view": {
"card": "kartica",
"table": "tabela",
"poster": "poster"
},
"general": {
"displayType": "tip prikaza",
"gap": "$t(common.gap)",
"tableColumns": "tabela kolona",
"autoFitColumns": "automatski uklopi kolone",
"size": "$t(common.size)"
},
"label": {
"releaseDate": "datum objavljivanja",
"title": "$t(common.title)",
"duration": "$t(common.duration)",
"titleCombined": "$t(common.title) (kombinovano)",
"dateAdded": "datum dodavanja",
"size": "$t(common.size)",
"bpm": "$t(common.bpm)",
"lastPlayed": "zadnje puštana",
"trackNumber": "broj pesme",
"rowIndex": "indeks reda",
"rating": "$t(common.rating)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
"note": "$t(common.note)",
"biography": "$t(common.biography)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"channels": "$t(common.channel_other)",
"playCount": "broj puštanja",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action_other)",
"genre": "$t(entity.genre_one)",
"discNumber": "disk broj",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
}
},
"column": {
"comment": "komentar",
"album": "album",
"rating": "rejting",
"favorite": "favorit",
"playCount": "puštanja",
"albumCount": "$t(entity.album_other)",
"releaseYear": "godina",
"lastPlayed": "zadnje puštana",
"biography": "biografija",
"releaseDate": "datum objavljivanja",
"bitrate": "bitrate",
"title": "naziv",
"bpm": "bpm",
"dateAdded": "datum dodavanja",
"artist": "$t(entity.artist_one)",
"songCount": "$t(entity.track_other)",
"trackNumber": "pesma",
"genre": "$t(entity.genre_one)",
"albumArtist": "album artist",
"path": "putanja",
"discNumber": "disk",
"channels": "$t(common.channel_other)"
}
},
"error": {
"remotePortWarning": "ponovo pokrenite server kako biste primenili novi port",
"systemFontError": "došlo je do greške prilikom pokušaja dobijanja sistema fontova",
"playbackError": "došlo je do greške prilikom pokušaja reprodukcije medija",
"endpointNotImplementedError": "krajnja tačka {{endpoint}} nije implementirana za {{serverType}}",
"remotePortError": "došlo je do greške prilikom postavljanja porta udaljenog servera",
"serverRequired": "potreban je server",
"authenticationFailed": "neuspešna autentikacija",
"apiRouteError": "nije moguće usmeriti zahtev",
"genericError": "došlo je do greške",
"credentialsRequired": "potrebni su pristupni podaci",
"sessionExpiredError": "vaša sesija je istekla",
"remoteEnableError": "došlo je do greške prilikom pokušaja omogućavanja udaljenog servera",
"localFontAccessDenied": "pristup lokalnim fontovima odbijen",
"serverNotSelectedError": "nije izabran nijedan server",
"remoteDisableError": "došlo je do greške prilikom pokušaja onemogućavanja udaljenog servera",
"mpvRequired": "MPV je obavezan",
"audioDeviceFetchError": "došlo je do greške prilikom pokušaja dobijanja audio uređaja",
"invalidServer": "neispravan server",
"loginRateError": "previše pokušaja prijave, molimo pokušajte ponovo za nekoliko sekundi"
},
"filter": {
"mostPlayed": "najviše puštana",
"comment": "komentar",
"playCount": "broj slušanja",
"recentlyUpdated": "skorije ažurirana",
"channels": "$t(common.channel_other)",
"isCompilation": "je kompilacija",
"recentlyPlayed": "skorije puštana",
"isRated": "je ocenjena",
"owner": "$t(common.owner)",
"title": "naziv",
"rating": "rejting",
"search": "pretraga",
"bitrate": "bitrejt",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "skorije dodata",
"note": "notacija",
"name": "ime",
"dateAdded": "datum dodavanja",
"releaseDate": "datum izdavanja",
"albumCount": "$t(entity.album_other) albuma",
"communityRating": "ocena zajednice",
"path": "putanja",
"favorited": "favoriti",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "je skorije puštana",
"isFavorited": "je favorit",
"bpm": "bpm",
"releaseYear": "godina izdavanja",
"id": "id",
"disc": "disk",
"biography": "biografija",
"songCount": "broj pesama",
"artist": "$t(entity.artist_one)",
"duration": "trajanje",
"isPublic": "je javna",
"random": "nasumično",
"lastPlayed": "zadnje puštana",
"toYear": "do godine",
"fromYear": "iz godine",
"criticRating": "ocena kritičara",
"album": "$t(entity.album_one)",
"trackNumber": "pesma"
},
"page": {
"sidebar": {
"nowPlaying": "trenutno pušta",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
},
"fullscreenPlayer": {
"config": {
"showLyricMatch": "prikaži poklapanje teksta",
"dynamicBackground": "dinamička pozadina",
"synchronized": "s sinhronizacijom",
"followCurrentLyric": "prati trenutni tekst pesme",
"opacity": "providnost",
"lyricSize": "veličina teksta pesme",
"showLyricProvider": "prikaži pružatelja teksta pesme",
"unsynchronized": "bez sinhronizacije",
"lyricAlignment": "poravnanje teksta pesme",
"useImageAspectRatio": "koristi odnos stranica slike",
"lyricGap": "razmak između stihova"
},
"upNext": "sledi",
"lyrics": "tekst pesme",
"related": "povezano"
},
"appMenu": {
"selectServer": "izaberi server",
"version": "verzija {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "upravljaj serverima",
"expandSidebar": "proširi bočnu traku",
"collapseSidebar": "skloni bočnu traku",
"openBrowserDevtools": "otvori alatke za razvoj pretraživača",
"quit": "$t(common.quit)",
"goBack": "idi nazad",
"goForward": "idi napred"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} izabrano",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"home": {
"mostPlayed": "najviše puštano",
"newlyAdded": "nedavno dodate pesme",
"title": "$t(common.home)",
"explore": "istraži iz tvoje biblioteke",
"recentlyPlayed": "nedavno puštane pesme"
},
"albumDetail": {
"moreFromArtist": "još od ovog $t(entity.genre_one)",
"moreFromGeneric": "još od {{item}}"
},
"setting": {
"playbackTab": "reprodukcija",
"generalTab": "opšte",
"hotkeysTab": "prečice",
"windowTab": "prozor"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
},
"globalSearch": {
"commands": {
"serverCommands": "komande servera",
"goToPage": "idi na stranicu",
"searchFor": "pretraži za {{query}}"
},
"title": "komande"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"form": {
"deletePlaylist": {
"title": "obriši $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) uspešno obrisan",
"input_confirm": "unesite ime $t(entity.playlist_one) za potvrdu"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "kreiraj $t(entity.playlist_one)",
"input_public": "javno",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) uspešno kreiran",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "dodaj server",
"input_username": "korisničko ime",
"input_url": "URL",
"input_password": "lozinka",
"input_legacyAuthentication": "omogući staru autentikaciju",
"input_name": "ime servera",
"success": "server uspešno dodat",
"input_savePassword": "sačuvaj lozinku",
"ignoreSsl": "ignoriši SSL ($t(common.restartRequired))",
"ignoreCors": "ignoriši CORS ($t(common.restartRequired))",
"error_savePassword": "došlo je do greške prilikom pokušaja čuvanja lozinke"
},
"addToPlaylist": {
"success": "dodato {{message}} $t(entity.song_other) u {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "dodaj u $t(entity.playlist_one)",
"input_skipDuplicates": "preskoči duplikate",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "ažuriraj server",
"success": "server uspešno ažuriran"
},
"queryEditor": {
"input_optionMatchAll": "pronađi sve",
"input_optionMatchAny": "pronađi bilo koji"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "pretraga teksta pesme"
},
"editPlaylist": {
"title": "izmeni $t(entity.playlist_one)"
}
},
"entity": {
"genre_one": "žanr",
"genre_few": "žanrova",
"genre_other": "žanrova",
"playlistWithCount_one": "{{count}} plejlista",
"playlistWithCount_few": "{{count}} plejlista",
"playlistWithCount_other": "{{count}} plejlista",
"playlist_one": "plejlista",
"playlist_few": "plejlista",
"playlist_other": "plejlista",
"artist_one": "umetnik",
"artist_few": "umetnika",
"artist_other": "umetnika",
"folderWithCount_one": "{{count}} folder",
"folderWithCount_few": "{{count}} foldera",
"folderWithCount_other": "{{count}} foldera",
"albumArtist_one": "album umetnika",
"albumArtist_few": "albuma umetnika",
"albumArtist_other": "albuma umetnika",
"track_one": "pesma",
"track_few": "pesama",
"track_other": "pesama",
"albumArtistCount_one": "{{count}} album umetnika",
"albumArtistCount_few": "{{count}} albuma umetnika",
"albumArtistCount_other": "{{count}} albuma umetnika",
"albumWithCount_one": "{{count}} album",
"albumWithCount_few": "{{count}} albuma",
"albumWithCount_other": "{{count}} albuma",
"favorite_one": "favorit",
"favorite_few": "favorita",
"favorite_other": "favorita",
"artistWithCount_one": "{{count}} umetnik",
"artistWithCount_few": "{{count}} umetnika",
"artistWithCount_other": "{{count}} umetnika",
"folder_one": "folder",
"folder_few": "foldera",
"folder_other": "foldera",
"smartPlaylist": "pametna $t(entity.playlist_one)",
"album_one": "album",
"album_few": "albumi",
"album_other": "albuma",
"genreWithCount_one": "{{count}} žanr",
"genreWithCount_few": "{{count}} žanrova",
"genreWithCount_other": "{{count}} žanrova",
"trackWithCount_one": "{{count}} pesma",
"trackWithCount_few": "{{count}} pesama",
"trackWithCount_other": "{{count}} pesama"
}
}
+345
View File
@@ -0,0 +1,345 @@
{
"action": {
"editPlaylist": "redigera $t(entity.playlist_one)",
"goToPage": "gå till sida",
"moveToTop": "flytta till toppen",
"clearQueue": "rensa kö",
"addToFavorites": "lägg till $t(entity.favorite_other)",
"addToPlaylist": "lägg till $t(entity.playlist_one)",
"createPlaylist": "skapa $t(entity.playlist_one)",
"removeFromPlaylist": "ta bort från $t(entity.playlist_one)",
"viewPlaylists": "visa $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "ta bort $t(entity.playlist_one)",
"removeFromQueue": "ta bort från kö",
"deselectAll": "avmarkera alla",
"moveToBottom": "flytta till botten",
"setRating": "sätt betyg",
"toggleSmartPlaylistEditor": "växla $t(entity.smartPlaylist) redigerare",
"removeFromFavorites": "ta bort från $t(entity.favorite_other)"
},
"common": {
"backward": "bakåt",
"increase": "öka",
"rating": "betyg",
"bpm": "bpm",
"refresh": "laddaom",
"unknown": "okänd",
"areYouSure": "är du säker?",
"edit": "redigera",
"favorite": "favorit",
"left": "vänster",
"save": "spara",
"right": "höger",
"currentSong": "aktuell $t(entity.track_one)",
"collapse": "kollaps",
"trackNumber": "spår",
"descending": "fallande",
"add": "lägg till",
"gap": "avstånd",
"ascending": "stigande",
"dismiss": "avskeda",
"year": "år",
"manage": "hantera",
"limit": "gräns",
"minimize": "minimera",
"modified": "modifierad",
"duration": "längd",
"name": "namn",
"maximize": "maximera",
"decrease": "minska",
"ok": "ok",
"description": "beskrivning",
"configure": "konfigurera",
"path": "sökväg",
"no": "nej",
"owner": "ägare",
"enable": "aktivera",
"clear": "töm",
"forward": "framåt",
"delete": "ta bort",
"cancel": "avbryt",
"forceRestartRequired": "starta om för att tillämpa ändringar... Stäng meddelandet för att starta om",
"setting": "inställning",
"version": "version",
"title": "titel",
"filter_one": "filter",
"filter_other": "filter",
"filters": "filter",
"create": "skapa",
"bitrate": "bithastighet",
"saveAndReplace": "spara och skrivöver",
"action_one": "handling",
"action_other": "handlingar",
"playerMustBePaused": "spelaren måste pausas",
"confirm": "bekräfta",
"resetToDefault": "återställ till standard",
"home": "hem",
"comingSoon": "kommer snart…",
"reset": "nollställ",
"channel_one": "kanal",
"channel_other": "kanaler",
"disable": "inaktivera",
"sortOrder": "ordning",
"none": "ingen",
"menu": "meny",
"restartRequired": "omstart krävs",
"previousSong": "föregående $t(entity.track_one)",
"noResultsFromQuery": "frågan returnerade inga resultat",
"quit": "avsluta",
"expand": "expandera",
"search": "sök",
"saveAs": "spara som",
"disc": "skiva",
"yes": "ja",
"random": "slumpmässig",
"size": "storlek",
"biography": "biografi",
"note": "anteckning",
"center": "center"
},
"error": {
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
"systemFontError": "ett fel uppstod vid försök att hämta systemteckensnitt",
"playbackError": "ett fel uppstod vid försök att spela upp media",
"endpointNotImplementedError": "endpoint {{endpoint}} är inte implementerad för {{serverType}}",
"remotePortError": "ett fel uppstod vid försök att ange serverporten",
"serverRequired": "server krävs",
"authenticationFailed": "autentiseringen misslyckades",
"apiRouteError": "det går inte att dirigera begäran",
"genericError": "ett fel uppstod",
"credentialsRequired": "autentiseringsuppgifter som krävs",
"sessionExpiredError": "din session har löpt ut",
"remoteEnableError": "Ett fel uppstod vid försök att $t(common.enable) servern",
"localFontAccessDenied": "åtkomst nekad till lokala teckensnitt",
"serverNotSelectedError": "ingen server vald",
"remoteDisableError": "ett fel uppstod vid försök av $t(common.disable) servern",
"mpvRequired": "MPV krävs",
"audioDeviceFetchError": "ett fel uppstod vid hämtning av ljudenheter",
"invalidServer": "ogiltig server",
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder"
},
"filter": {
"mostPlayed": "mest spelade",
"comment": "kommentar",
"playCount": "antal spelningar",
"recentlyUpdated": "nyligen uppdaterad",
"channels": "$t(common.channel_other)",
"isCompilation": "är kompilering",
"recentlyPlayed": "nyligen spelad",
"isRated": "är betygsatt",
"owner": "$t(common.owner)",
"title": "titel",
"rating": "betyg",
"search": "sök",
"bitrate": "bithastighet",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "nyligen tillagda",
"note": "anteckning",
"name": "namn",
"dateAdded": "datum tillagt",
"releaseDate": "utgivningsdag",
"communityRating": "betyg från communityn",
"path": "sökväg",
"favorited": "favoritmärkt",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "spelas nyligen",
"isFavorited": "är favoritmärkt",
"bpm": "bpm",
"releaseYear": "utgivningsår",
"id": "id",
"disc": "skiva",
"biography": "biografi",
"artist": "$t(entity.artist_one)",
"duration": "längd",
"isPublic": "är offentlig",
"random": "slumpmässig",
"lastPlayed": "senast spelad",
"toYear": "till år",
"fromYear": "från år",
"album": "$t(entity.album_one)",
"trackNumber": "spår",
"songCount": "sångräkning",
"criticRating": "kritikerbetyg"
},
"form": {
"deletePlaylist": {
"title": "ta bort $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) har tagits bort",
"input_confirm": "Skriv namnet på $t(entity.playlist_one) för att bekräfta"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "skapa $t(entity.playlist_one)",
"input_public": "offentlig",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) skapad",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "lägg till server",
"input_username": "användarnamn",
"input_url": "länk",
"input_password": "lösenord",
"input_legacyAuthentication": "aktivera äldre autentisering",
"input_name": "server namn",
"success": "servern har lagts till",
"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"
},
"addToPlaylist": {
"success": "tillade {{message}} $t(entity.song_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "lägg till i $t(entity.playlist_one)",
"input_skipDuplicates": "hoppa över dubbletter",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "uppdatera server",
"success": "servern har uppdaterats"
},
"queryEditor": {
"input_optionMatchAll": "matcha alla",
"input_optionMatchAny": "matcha något"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "sångtext sök"
},
"editPlaylist": {
"title": "redigera $t(entity.playlist_one)"
}
},
"page": {
"fullscreenPlayer": {
"config": {
"showLyricMatch": "Visa låttext matchning",
"dynamicBackground": "dynamisk bakgrund",
"followCurrentLyric": "följ aktuell låttext",
"opacity": "ogenomskinlighet",
"lyricSize": "låttext storlek",
"lyricAlignment": "låttext justering",
"lyricGap": "låttext mellanrum",
"synchronized": "synkroniserad",
"showLyricProvider": "visa sångtextleverantör",
"unsynchronized": "osynkroniserad"
},
"lyrics": "sångtext",
"related": "relaterad"
},
"appMenu": {
"selectServer": "välj server",
"version": "version {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "hantera servrar",
"expandSidebar": "expandera sidofältet",
"openBrowserDevtools": "öppna webbläsarens utvecklingsverktyg",
"quit": "$t(common.quit)",
"goBack": "gå tillbaka",
"goForward": "gå framåt",
"collapseSidebar": "växla sidofältet"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} vald",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"albumDetail": {
"moreFromArtist": "mer från $t(entity.genre_one)",
"moreFromGeneric": "mer från {{item}}"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
},
"sidebar": {
"nowPlaying": "nu spelas"
},
"home": {
"mostPlayed": "mest spelade",
"newlyAdded": "nytillkomna utgåvor",
"explore": "utforska från ditt bibliotek",
"recentlyPlayed": "nyligen spelat"
},
"setting": {
"playbackTab": "uppspelning",
"generalTab": "allmänt",
"hotkeysTab": "snabbtangenter",
"windowTab": "fönster"
},
"globalSearch": {
"commands": {
"serverCommands": "serverkommandon",
"goToPage": "gå till sidan",
"searchFor": "sök efter {{query}}"
},
"title": "kommandon"
}
},
"entity": {
"playlist_one": "spellista",
"playlist_other": "spellistor",
"artist_one": "artist",
"artist_other": "artister",
"albumArtist_one": "albumartist",
"albumArtist_other": "albumartister",
"albumArtistCount_one": "{{count}} Albumartist",
"albumArtistCount_other": "{{count}} Albumartister",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} album",
"favorite_one": "favorit",
"favorite_other": "favoriter",
"folder_one": "mapp",
"folder_other": "mappar",
"album_one": "album",
"album_other": "album",
"playlistWithCount_one": "{{count}} spellista",
"playlistWithCount_other": "{{count}} spellistor",
"folderWithCount_one": "{{count}} mapp",
"folderWithCount_other": "{{count}} mappar",
"track_one": "spår",
"track_other": "spår",
"trackWithCount_one": "{{count}} spår",
"trackWithCount_other": "{{count}} spår"
},
"player": {
"repeat_all": "repetera alla",
"repeat": "repetera",
"queue_remove": "ta bort markerad",
"playRandom": "spela slumpmässigt",
"previous": "föregående",
"favorite": "favorit",
"next": "nästa",
"shuffle": "blanda",
"playbackFetchNoResults": "inga låtar hittades",
"playbackFetchInProgress": "laddar låtar…",
"addNext": "lägg till nästa",
"playbackSpeed": "uppspelningshastighet",
"playbackFetchCancel": "det här tar ett tag... stäng aviseringen för att avbryta",
"play": "spela",
"repeat_off": "repetera inaktiverad",
"queue_clear": "rensa kö",
"muted": "mutad",
"queue_moveToTop": "flytta markerad till botten",
"queue_moveToBottom": "flytta markerad till toppen",
"addLast": "lägg till sist",
"mute": "muta"
}
}
+5 -5
View File
@@ -109,7 +109,7 @@
"favorite_other": "收藏",
"artistWithCount_other": "{{count}} 位艺术家",
"folder_other": "文件夹",
"smartPlaylist": "智能 $t(entity.playlist_one)",
"smartPlaylist": "智能$t(entity.playlist_one)",
"genreWithCount_other": "{{count}} 种流派",
"trackWithCount_other": "{{count}} 首乐曲"
},
@@ -120,7 +120,7 @@
"queue_remove": "移除所选",
"playRandom": "随机播放",
"skip": "跳过",
"previous": "一首",
"previous": "一首",
"toggleFullscreenPlayer": "全屏",
"skip_back": "向后跳过",
"favorite": "收藏",
@@ -208,7 +208,7 @@
"hotkey_skipForward": "向后跳过",
"sidePlayQueueStyle": "侧边播放列表样式",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"zoom": "放率",
"zoom": "放率",
"minimizeToTray_description": "将应用程序最小化到系统托盘",
"hotkey_playbackPlay": "播放",
"hotkey_togglePreviousSongFavorite": "收藏 / 取消收藏$t(common.previousSong)",
@@ -233,7 +233,7 @@
"hotkey_toggleFullScreenPlayer": "全屏播放",
"hotkey_localSearch": "页面内搜索",
"hotkey_toggleQueue": "显示 / 隐藏播放队列",
"zoom_description": "设置应用的放大率",
"zoom_description": "设置应用程序的缩放率",
"remotePassword_description": "设置远程控制服务器的密码。这些凭据默认以不安全的方式传输,因此您应该使用一个您不在意的唯一密码",
"hotkey_rate5": "评为 5 星",
"hotkey_playbackPrevious": "上一曲",
@@ -424,7 +424,7 @@
"title": "$t(common.home)"
},
"albumDetail": {
"moreFromArtist": "更多该$t(entity.genre_one)作品",
"moreFromArtist": "更多该$t(entity.artist_one)作品",
"moreFromGeneric": "更多{{item}}作品"
},
"setting": {
+41
View File
@@ -0,0 +1,41 @@
import dayjs from 'dayjs';
const reset = '\x1b[0m';
const baseLog = (errorType: 'error' | 'info' | 'success' | 'warn') => {
let logString = '';
switch (errorType) {
case 'error':
logString = '\x1b[31m[ERROR] ';
break;
case 'info':
logString = '\x1b[34m[INFO] ';
break;
case 'success':
logString = '\x1b[32m[SUCCESS] ';
break;
case 'warn':
logString = '\x1b[33m[WARNING] ';
break;
default:
logString = '\x1b[34m[INFO] ';
break;
}
return (text: string, options?: { context?: Record<string, any>; toast?: boolean }): void => {
// const { toast } = options || {};
const now = dayjs().toISOString();
console.log(
`${logString}${now}: ${text} | ${
options?.context && JSON.stringify(options.context)
}${reset}`,
);
};
};
export const fsLog = {
error: baseLog('error'),
info: baseLog('info'),
success: baseLog('success'),
warn: baseLog('warn'),
};
+40 -34
View File
@@ -84,16 +84,6 @@ const installExtensions = async () => {
.catch(console.log);
};
const singleInstance = app.requestSingleInstanceLock();
if (!singleInstance) {
app.quit();
} else {
app.on('second-instance', () => {
mainWindow?.show();
});
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
@@ -666,31 +656,47 @@ const FONT_HEADERS = [
'font/woff2',
];
app.whenReady()
.then(() => {
protocol.handle('feishin', async (request) => {
const filePath = `file://${request.url.slice('feishin://'.length)}`;
const response = await net.fetch(filePath);
const contentType = response.headers.get('content-type');
const singleInstance = app.requestSingleInstanceLock();
if (!contentType || !FONT_HEADERS.includes(contentType)) {
getMainWindow()?.webContents.send('custom-font-error', filePath);
return new Response(null, {
status: 403,
statusText: 'Forbidden',
});
if (!singleInstance) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
return response;
});
mainWindow.focus();
}
});
createWindow();
createTray();
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
})
.catch(console.log);
app.whenReady()
.then(() => {
protocol.handle('feishin', async (request) => {
const filePath = `file://${request.url.slice('feishin://'.length)}`;
const response = await net.fetch(filePath);
const contentType = response.headers.get('content-type');
if (!contentType || !FONT_HEADERS.includes(contentType)) {
getMainWindow()?.webContents.send('custom-font-error', filePath);
return new Response(null, {
status: 403,
statusText: 'Forbidden',
});
}
return response;
});
createWindow();
createTray();
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
})
.catch(console.log);
}
+66 -189
View File
@@ -1,100 +1,38 @@
import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast/index';
import { RandomSongListArgs } from './types';
import i18n from '/@/i18n/i18n';
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import type {
AlbumDetailArgs,
AlbumListArgs,
SongListArgs,
SongDetailArgs,
AddToPlaylistArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
SetRatingArgs,
GenreListArgs,
AlbumDetailArgs,
AlbumListArgs,
ArtistListArgs,
ControllerEndpoint,
CreatePlaylistArgs,
DeletePlaylistArgs,
FavoriteArgs,
GenreListArgs,
LyricsArgs,
MusicFolderListArgs,
PlaylistDetailArgs,
PlaylistListArgs,
MusicFolderListArgs,
PlaylistSongListArgs,
ArtistListArgs,
RemoveFromPlaylistArgs,
ScrobbleArgs,
SearchArgs,
SetRatingArgs,
SongDetailArgs,
SongListArgs,
TopSongListArgs,
UpdatePlaylistArgs,
UserListArgs,
FavoriteArgs,
TopSongListArgs,
AddToPlaylistArgs,
AddToPlaylistResponse,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
ScrobbleArgs,
ScrobbleResponse,
AlbumArtistDetailResponse,
FavoriteResponse,
CreatePlaylistResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
ArtistListResponse,
GenreListResponse,
MusicFolderListResponse,
PlaylistDetailResponse,
PlaylistListResponse,
RatingResponse,
SongDetailResponse,
SongListResponse,
TopSongListResponse,
UpdatePlaylistResponse,
UserListResponse,
AuthenticationResponse,
SearchArgs,
SearchResponse,
LyricsArgs,
LyricsResponse,
} from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast/index';
import { useAuthStore } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types';
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import i18n from '/@/i18n/i18n';
export type ControllerEndpoint = Partial<{
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { password: string; username: string },
) => Promise<AuthenticationResponse>;
clearPlaylist: () => void;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getArtistDetail: () => void;
getArtistInfo: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
}>;
type ApiController = {
jellyfin: ControllerEndpoint;
@@ -103,110 +41,9 @@ type ApiController = {
};
const endpoints: ApiController = {
jellyfin: {
addToPlaylist: jfController.addToPlaylist,
authenticate: jfController.authenticate,
clearPlaylist: undefined,
createFavorite: jfController.createFavorite,
createPlaylist: jfController.createPlaylist,
deleteFavorite: jfController.deleteFavorite,
deletePlaylist: jfController.deletePlaylist,
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
getAlbumArtistList: jfController.getAlbumArtistList,
getAlbumDetail: jfController.getAlbumDetail,
getAlbumList: jfController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: jfController.getGenreList,
getLyrics: jfController.getLyrics,
getMusicFolderList: jfController.getMusicFolderList,
getPlaylistDetail: jfController.getPlaylistDetail,
getPlaylistList: jfController.getPlaylistList,
getPlaylistSongList: jfController.getPlaylistSongList,
getRandomSongList: jfController.getRandomSongList,
getSongDetail: jfController.getSongDetail,
getSongList: jfController.getSongList,
getTopSongs: jfController.getTopSongList,
getUserList: undefined,
removeFromPlaylist: jfController.removeFromPlaylist,
scrobble: jfController.scrobble,
search: jfController.search,
setRating: undefined,
updatePlaylist: jfController.updatePlaylist,
},
navidrome: {
addToPlaylist: ndController.addToPlaylist,
authenticate: ndController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: ndController.createPlaylist,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: ndController.deletePlaylist,
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
getAlbumArtistList: ndController.getAlbumArtistList,
getAlbumDetail: ndController.getAlbumDetail,
getAlbumList: ndController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: ndController.getGenreList,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: ndController.getPlaylistDetail,
getPlaylistList: ndController.getPlaylistList,
getPlaylistSongList: ndController.getPlaylistSongList,
getRandomSongList: ssController.getRandomSongList,
getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList,
getTopSongs: ssController.getTopSongList,
getUserList: ndController.getUserList,
removeFromPlaylist: ndController.removeFromPlaylist,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: ssController.setRating,
updatePlaylist: ndController.updatePlaylist,
},
subsonic: {
authenticate: ssController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: undefined,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: undefined,
getAlbumArtistDetail: undefined,
getAlbumArtistList: undefined,
getAlbumDetail: undefined,
getAlbumList: undefined,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongDetail: undefined,
getSongList: undefined,
getTopSongs: ssController.getTopSongList,
getUserList: undefined,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: undefined,
updatePlaylist: undefined,
},
jellyfin: JellyfinController,
navidrome: NavidromeController,
subsonic: SubsonicController,
};
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
@@ -259,6 +96,15 @@ const getAlbumList = async (args: AlbumListArgs) => {
)?.(args);
};
const getAlbumListCount = async (args: AlbumListArgs) => {
return (
apiController(
'getAlbumListCount',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumListCount']
)?.(args);
};
const getAlbumDetail = async (args: AlbumDetailArgs) => {
return (
apiController(
@@ -277,6 +123,15 @@ const getSongList = async (args: SongListArgs) => {
)?.(args);
};
const getSongListCount = async (args: SongListArgs) => {
return (
apiController(
'getSongListCount',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongListCount']
)?.(args);
};
const getSongDetail = async (args: SongDetailArgs) => {
return (
apiController(
@@ -322,6 +177,15 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
)?.(args);
};
const getAlbumArtistListCount = async (args: AlbumArtistListArgs) => {
return (
apiController(
'getAlbumArtistListCount',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistListCount']
)?.(args);
};
const getArtistList = async (args: ArtistListArgs) => {
return (
apiController(
@@ -340,6 +204,15 @@ const getPlaylistList = async (args: PlaylistListArgs) => {
)?.(args);
};
const getPlaylistListCount = async (args: PlaylistListArgs) => {
return (
apiController(
'getPlaylistListCount',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistListCount']
)?.(args);
};
const createPlaylist = async (args: CreatePlaylistArgs) => {
return (
apiController(
@@ -490,18 +363,22 @@ export const controller = {
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumArtistListCount,
getAlbumDetail,
getAlbumList,
getAlbumListCount,
getArtistList,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistListCount,
getPlaylistSongList,
getRandomSongList,
getSongDetail,
getSongList,
getSongListCount,
getTopSongList,
getUserList,
removeFromPlaylist,
@@ -7,6 +7,7 @@ import { ServerListItem } from '/@/renderer/types';
import omitBy from 'lodash/omitBy';
import { z } from 'zod';
import { authenticationFailure } from '/@/renderer/api/utils';
import i18n from '/@/i18n/i18n';
const c = initContract();
@@ -337,6 +338,14 @@ export const jfApiClient = (args: {
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(
i18n.t('error.networkError', {
postProcess: 'sentenceCase',
}) as string,
);
}
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
+227 -62
View File
@@ -1,62 +1,64 @@
import isElectron from 'is-electron';
import { z } from 'zod';
import packageJson from '../../../../package.json';
import { jfNormalize } from './jellyfin-normalize';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import {
AuthenticationResponse,
MusicFolderListArgs,
MusicFolderListResponse,
GenreListArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
albumArtistListSortMap,
sortOrderMap,
ArtistListArgs,
artistListSortMap,
AlbumDetailArgs,
AlbumListArgs,
albumListSortMap,
TopSongListArgs,
SongListArgs,
songListSortMap,
AddToPlaylistArgs,
RemoveFromPlaylistArgs,
PlaylistDetailArgs,
PlaylistSongListArgs,
PlaylistListArgs,
playlistListSortMap,
AddToPlaylistResponse,
AlbumArtistDetailArgs,
AlbumArtistDetailResponse,
AlbumArtistListArgs,
AlbumArtistListResponse,
AlbumDetailArgs,
AlbumDetailResponse,
AlbumListArgs,
AlbumListResponse,
AuthenticationResponse,
ControllerEndpoint,
CreatePlaylistArgs,
CreatePlaylistResponse,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
DeletePlaylistArgs,
FavoriteArgs,
FavoriteResponse,
ScrobbleArgs,
ScrobbleResponse,
GenreListArgs,
GenreListResponse,
AlbumArtistDetailResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
SongListResponse,
AddToPlaylistResponse,
RemoveFromPlaylistResponse,
PlaylistDetailResponse,
PlaylistListResponse,
SearchArgs,
SearchResponse,
RandomSongListResponse,
RandomSongListArgs,
LyricsArgs,
LyricsResponse,
genreListSortMap,
MusicFolderListArgs,
MusicFolderListResponse,
PlaylistDetailArgs,
PlaylistDetailResponse,
PlaylistListArgs,
PlaylistListResponse,
PlaylistSongListArgs,
RandomSongListArgs,
RandomSongListResponse,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
ScrobbleArgs,
ScrobbleResponse,
SearchArgs,
SearchResponse,
SongDetailArgs,
SongDetailResponse,
SongListArgs,
SongListResponse,
SongListSort,
SortOrder,
TopSongListArgs,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
albumArtistListSortMap,
albumListSortMap,
genreListSortMap,
playlistListSortMap,
songListSortMap,
sortOrderMap,
} from '/@/renderer/api/types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfNormalize } from './jellyfin-normalize';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import packageJson from '../../../../package.json';
import { z } from 'zod';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import isElectron from 'is-electron';
import { sortSongList } from '/@/renderer/api/utils';
const formatCommaDelimitedString = (value: string[]) => {
return value.join(',');
@@ -244,31 +246,56 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtis
};
};
const getArtistList = async (args: ArtistListArgs): Promise<AlbumArtistListResponse> => {
const getAlbumArtistListCount = async (args: AlbumArtistListArgs): Promise<number> => {
const { query, apiClientProps } = args;
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
query: {
Limit: query.limit,
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
ImageTypeLimit: 1,
Limit: 1,
ParentId: query.musicFolderId,
Recursive: true,
SortBy: artistListSortMap.jellyfin[query.sortBy] || 'Name,SortName',
SearchTerm: query.searchTerm,
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'Name,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.startIndex,
StartIndex: 0,
UserId: apiClientProps.server?.userId || undefined,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist list');
throw new Error('Failed to get album artist list count');
}
return {
items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
startIndex: query.startIndex,
totalRecordCount: res.body.TotalRecordCount,
};
return res.body.TotalRecordCount;
};
// const getArtistList = async (args: ArtistListArgs): Promise<ArtistListResponse> => {
// const { query, apiClientProps } = args;
// const res = await jfApiClient(apiClientProps).getAlbumArtistList({
// query: {
// Limit: query.limit,
// ParentId: query.musicFolderId,
// Recursive: true,
// SortBy: artistListSortMap.jellyfin[query.sortBy] || 'Name,SortName',
// SortOrder: sortOrderMap.jellyfin[query.sortOrder],
// StartIndex: query.startIndex,
// },
// });
// if (res.status !== 200) {
// throw new Error('Failed to get artist list');
// }
// return {
// items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
// startIndex: query.startIndex,
// totalRecordCount: res.body.TotalRecordCount,
// };
// };
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
const { query, apiClientProps } = args;
@@ -333,6 +360,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> =>
AlbumArtistIds: query.artistIds
? formatCommaDelimitedString(query.artistIds)
: undefined,
ContributingArtistIds: query.isCompilation ? query.artistIds?.[0] : undefined,
IncludeItemTypes: 'MusicAlbum',
Limit: query.limit,
ParentId: query.musicFolderId,
@@ -357,6 +385,55 @@ const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> =>
};
};
const getAlbumListCount = async (args: AlbumListArgs): Promise<number> => {
const { query, apiClientProps } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
}
const yearsGroup = [];
if (query._custom?.jellyfin?.minYear && query._custom?.jellyfin?.maxYear) {
for (
let i = Number(query._custom?.jellyfin?.minYear);
i <= Number(query._custom?.jellyfin?.maxYear);
i += 1
) {
yearsGroup.push(String(i));
}
}
const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined;
const res = await jfApiClient(apiClientProps).getAlbumList({
params: {
userId: apiClientProps.server?.userId,
},
query: {
AlbumArtistIds: query.artistIds
? formatCommaDelimitedString(query.artistIds)
: undefined,
ContributingArtistIds: query.isCompilation ? query.artistIds?.[0] : undefined,
IncludeItemTypes: 'MusicAlbum',
Limit: 1,
ParentId: query.musicFolderId,
Recursive: true,
SearchTerm: query.searchTerm,
SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: 0,
...query._custom?.jellyfin,
Years: yearsFilter,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album list count');
}
return res.body.TotalRecordCount;
};
const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse> => {
const { apiClientProps, query } = args;
@@ -384,8 +461,11 @@ const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse>
throw new Error('Failed to get top song list');
}
const songs = res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, ''));
const songsByPlayCount = sortSongList(songs, SongListSort.PLAY_COUNT, SortOrder.DESC);
return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
items: songsByPlayCount,
startIndex: 0,
totalRecordCount: res.body.TotalRecordCount,
};
@@ -449,6 +529,58 @@ const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
};
};
const getSongListCount = async (args: SongListArgs): Promise<number> => {
const { query, apiClientProps } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
}
const yearsGroup = [];
if (query._custom?.jellyfin?.minYear && query._custom?.jellyfin?.maxYear) {
for (
let i = Number(query._custom?.jellyfin?.minYear);
i <= Number(query._custom?.jellyfin?.maxYear);
i += 1
) {
yearsGroup.push(String(i));
}
}
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
const albumIdsFilter = query.albumIds ? formatCommaDelimitedString(query.albumIds) : undefined;
const artistIdsFilter = query.artistIds
? formatCommaDelimitedString(query.artistIds)
: undefined;
const res = await jfApiClient(apiClientProps).getSongList({
params: {
userId: apiClientProps.server?.userId,
},
query: {
AlbumIds: albumIdsFilter,
ArtistIds: artistIdsFilter,
Fields: 'Genres, DateCreated, MediaSources, ParentId',
IncludeItemTypes: 'Audio',
Limit: 1,
ParentId: query.musicFolderId,
Recursive: true,
SearchTerm: query.searchTerm,
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: 0,
...query._custom?.jellyfin,
Years: yearsFilter,
},
});
if (res.status !== 200) {
throw new Error('Failed to get song list count');
}
return res.body.TotalRecordCount;
};
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResponse> => {
const { query, body, apiClientProps } = args;
@@ -535,7 +667,6 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<SongList
query: {
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
IncludeItemTypes: 'Audio',
Limit: query.limit,
SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined,
SortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined,
StartIndex: 0,
@@ -549,7 +680,7 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<SongList
return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
startIndex: query.startIndex,
startIndex: 0,
totalRecordCount: res.body.TotalRecordCount,
};
};
@@ -589,6 +720,37 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
};
};
const getPlaylistListCount = async (args: PlaylistListArgs): Promise<number> => {
const { query, apiClientProps } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
}
const res = await jfApiClient(apiClientProps).getPlaylistList({
params: {
userId: apiClientProps.server?.userId,
},
query: {
Fields: 'ChildCount, Genres, DateCreated, ParentId, Overview',
IncludeItemTypes: 'Playlist',
Limit: 1,
MediaTypes: 'Audio',
Recursive: true,
SearchTerm: query.searchTerm,
SortBy: playlistListSortMap.jellyfin[query.sortBy],
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: 0,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist list count');
}
return res.body.TotalRecordCount;
};
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
const { body, apiClientProps } = args;
@@ -946,7 +1108,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse>
return jfNormalize.song(res.body, apiClientProps.server, '');
};
export const jfController = {
export const JellyfinController: ControllerEndpoint = {
addToPlaylist,
authenticate,
createFavorite,
@@ -955,19 +1117,22 @@ export const jfController = {
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumArtistListCount,
getAlbumDetail,
getAlbumList,
getArtistList,
getAlbumListCount,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistListCount,
getPlaylistSongList,
getRandomSongList,
getSongDetail,
getSongList,
getTopSongList,
getSongListCount,
getTopSongs: getTopSongList,
removeFromPlaylist,
scrobble,
search,
+11 -3
View File
@@ -380,12 +380,20 @@ export const ndApiClient = (args: {
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(
i18n.t('error.networkError', {
postProcess: 'sentenceCase',
}) as string,
);
}
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: { data: response.data, headers: response.headers },
headers: response.headers as any,
status: response.status,
body: { data: response?.data, headers: response?.headers },
headers: response?.headers as any,
status: response?.status,
};
}
throw e;
@@ -39,11 +39,13 @@ import {
RemoveFromPlaylistResponse,
RemoveFromPlaylistArgs,
genreListSortMap,
ControllerEndpoint,
} from '../types';
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
const authenticate = async (
url: string,
@@ -129,7 +131,7 @@ const getAlbumArtistDetail = async (
},
});
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
const artistInfoRes = await subsonicApiClient(apiClientProps).getArtistInfo({
query: {
count: 10,
id: query.id,
@@ -148,15 +150,16 @@ const getAlbumArtistDetail = async (
{
...res.body.data,
...(artistInfoRes.status === 200 && {
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
similarArtists: artistInfoRes.body['subsonic-response'].artistInfo.similarArtist,
...(!res.body.data.largeImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
largeImageUrl: artistInfoRes.body['subsonic-response'].artistInfo.largeImageUrl,
}),
...(!res.body.data.mediumImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
largeImageUrl:
artistInfoRes.body['subsonic-response'].artistInfo.mediumImageUrl,
}),
...(!res.body.data.smallImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
largeImageUrl: artistInfoRes.body['subsonic-response'].artistInfo.smallImageUrl,
}),
}),
},
@@ -191,6 +194,27 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtis
};
};
const getAlbumArtistListCount = async (args: AlbumArtistListArgs): Promise<number> => {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
_end: 1,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: 0,
name: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album artist list count');
}
return Number(res.body.headers.get('x-total-count') || 0);
};
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
const { query, apiClientProps } = args;
@@ -230,6 +254,8 @@ const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> =>
_sort: albumListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
artist_id: query.artistIds?.[0],
compilation: query.isCompilation,
genre_id: query.genre,
name: query.searchTerm,
...query._custom?.navidrome,
},
@@ -246,6 +272,30 @@ const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> =>
};
};
const getAlbumListCount = async (args: AlbumListArgs): Promise<number> => {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
_end: 1,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumListSortMap.navidrome[query.sortBy],
_start: 0,
artist_id: query.artistIds?.[0],
compilation: query.isCompilation,
genre_id: query.genre,
name: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album list');
}
return Number(res.body.headers.get('x-total-count') || 0);
};
const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
const { query, apiClientProps } = args;
@@ -275,6 +325,29 @@ const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
};
};
const getSongListCount = async (args: SongListArgs): Promise<number> => {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getSongList({
query: {
_end: 1,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy],
_start: 0,
album_artist_id: query.artistIds,
album_id: query.albumIds,
title: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get song list count');
}
return Number(res.body.headers.get('x-total-count') || 0);
};
const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse> => {
const { query, apiClientProps } = args;
@@ -298,7 +371,7 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
body: {
comment: body.comment,
name: body.name,
public: body._custom?.navidrome?.public,
public: body.public,
rules: body._custom?.navidrome?.rules,
sync: body._custom?.navidrome?.sync,
},
@@ -322,7 +395,7 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
name: body.name,
public: body._custom?.navidrome?.public || false,
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
sync: body._custom?.navidrome?.sync || undefined,
sync: body._custom?.navidrome?.sync,
},
params: {
id: query.id,
@@ -360,7 +433,9 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_sort: query.sortBy
? playlistListSortMap.navidrome[query.sortBy]
: playlistListSortMap.navidrome.name,
_start: query.startIndex,
q: query.searchTerm,
...query._custom?.navidrome,
@@ -378,6 +453,29 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
};
};
const getPlaylistListCount = async (args: PlaylistListArgs): Promise<number> => {
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getPlaylistList({
query: {
_end: 1,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy
? playlistListSortMap.navidrome[query.sortBy]
: playlistListSortMap.navidrome.name,
_start: 0,
q: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist list count');
}
return Number(res.body.headers.get('x-total-count') || 0);
};
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<PlaylistDetailResponse> => {
const { query, apiClientProps } = args;
@@ -404,12 +502,11 @@ const getPlaylistSongList = async (
id: query.id,
},
query: {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
_sort: query.sortBy
? songListSortMap.navidrome[query.sortBy]
: ndType._enum.songList.ID,
_start: query.startIndex,
_start: 0,
},
});
@@ -419,7 +516,7 @@ const getPlaylistSongList = async (
return {
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
startIndex: query?.startIndex || 0,
startIndex: 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
@@ -465,22 +562,41 @@ const removeFromPlaylist = async (
return null;
};
export const ndController = {
export const NavidromeController: ControllerEndpoint = {
addToPlaylist,
authenticate,
clearPlaylist: undefined,
createFavorite: SubsonicController.createFavorite,
createPlaylist,
deleteFavorite: SubsonicController.deleteFavorite,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumArtistListCount,
getAlbumDetail,
getAlbumList,
getAlbumListCount,
getArtistDetail: undefined,
getArtistInfo: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList,
getMusicFolderList: SubsonicController.getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistListCount,
getPlaylistSongList,
getRandomSongList: SubsonicController.getRandomSongList,
getSongDetail,
getSongList,
getSongListCount,
getTopSongs: SubsonicController.getTopSongs,
getUserList,
removeFromPlaylist,
scrobble: SubsonicController.scrobble,
search: SubsonicController.search,
setRating: SubsonicController.setRating,
updatePlaylist,
};
@@ -11,8 +11,8 @@ import {
import { ServerListItem, ServerType } from '/@/renderer/types';
import z from 'zod';
import { ndType } from './navidrome-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { NDGenre } from '/@/renderer/api/navidrome.types';
import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
const getImageUrl = (args: { url: string | null }) => {
const { url } = args;
@@ -45,6 +45,14 @@ const getCoverArtUrl = (args: {
);
};
interface WithDate {
playDate?: string;
}
const normalizePlayDate = (item: WithDate): string | null => {
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
};
const normalizeSong = (
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
@@ -100,7 +108,7 @@ const normalizeSong = (
imagePlaceholderUrl,
imageUrl,
itemType: LibraryItem.SONG,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
lastPlayedAt: normalizePlayDate(item),
lyrics: item.lyrics ? item.lyrics : null,
name: item.title,
path: item.path,
@@ -159,7 +167,7 @@ const normalizeAlbum = (
imageUrl,
isCompilation: item.compilation,
itemType: LibraryItem.ALBUM,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
lastPlayedAt: normalizePlayDate(item),
name: item.name,
playCount: item.playCount,
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
@@ -178,7 +186,9 @@ const normalizeAlbum = (
const normalizeAlbumArtist = (
item: z.infer<typeof ndType._response.albumArtist> & {
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
similarArtists?: z.infer<
typeof SubsonicApi.getArtistInfo2.response
>['subsonic-response']['artistInfo2']['similarArtist'];
},
server: ServerListItem | null,
): AlbumArtist => {
@@ -207,7 +217,7 @@ const normalizeAlbumArtist = (
id: item.id,
imageUrl: imageUrl || null,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
lastPlayedAt: normalizePlayDate(item),
name: item.name,
playCount: item.playCount,
serverId: server?.id || 'unknown',
@@ -78,7 +78,7 @@ const albumArtist = z.object({
name: z.string(),
orderArtistName: z.string(),
playCount: z.number(),
playDate: z.string(),
playDate: z.string().optional(),
rating: z.number(),
size: z.number(),
smallImageUrl: z.string().optional(),
@@ -128,7 +128,7 @@ const album = z.object({
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
playCount: z.number(),
playDate: z.string(),
playDate: z.string().optional(),
rating: z.number().optional(),
size: z.number(),
songCount: z.number(),
@@ -211,7 +211,7 @@ const song = z.object({
orderTitle: z.string(),
path: z.string(),
playCount: z.number(),
playDate: z.string(),
playDate: z.string().optional(),
rating: z.number().optional(),
rgAlbumGain: z.number().optional(),
rgAlbumPeak: z.number().optional(),
+49 -6
View File
@@ -49,6 +49,19 @@ export const queryKeys: Record<
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
> = {
albumArtists: {
count: (serverId: string, query?: AlbumArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
}
if (query) {
return [serverId, 'albumArtists', 'count', filter] as const;
}
return [serverId, 'albumArtists', 'count'] as const;
},
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
return [serverId, 'albumArtists', 'detail'] as const;
@@ -72,23 +85,40 @@ export const queryKeys: Record<
},
},
albums: {
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination && artistId) {
return [serverId, 'albums', 'list', artistId, filter, pagination] as const;
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
}
if (query && pagination) {
return [serverId, 'albums', 'list', filter, pagination] as const;
return [serverId, 'albums', 'count', filter, pagination] as const;
}
if (query && artistId) {
return [serverId, 'albums', 'list', artistId, filter] as const;
return [serverId, 'albums', 'count', artistId, filter] as const;
}
if (query) {
return [serverId, 'albums', 'count', filter] as const;
}
return [serverId, 'albums', 'count'] as const;
},
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (
serverId: string,
query?: {
artistIds?: string[];
maxYear?: number;
minYear?: number;
searchTerm?: string;
},
) => {
const { filter } = splitPaginatedQuery(query);
if (query) {
return [serverId, 'albums', 'list', filter] as const;
}
@@ -207,6 +237,19 @@ export const queryKeys: Record<
root: (serverId: string) => [serverId] as const,
},
songs: {
count: (serverId: string, query?: AlbumArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'songs', 'count', filter, pagination] as const;
}
if (query) {
return [serverId, 'songs', 'count', filter] as const;
}
return [serverId, 'songs', 'count'] as const;
},
detail: (serverId: string, query?: SongDetailQuery) => {
if (query) return [serverId, 'songs', 'detail', query] as const;
return [serverId, 'songs', 'detail'] as const;
+388 -44
View File
@@ -1,93 +1,426 @@
import { initClient, initContract } from '@ts-rest/core';
import axios, { Method, AxiosError, isAxiosError, AxiosResponse } from 'axios';
import axios, { AxiosError, AxiosResponse, Method, isAxiosError } from 'axios';
import omitBy from 'lodash/omitBy';
import qs from 'qs';
import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import i18n from '/@/i18n/i18n';
import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast/index';
import i18n from '/@/i18n/i18n';
const c = initContract();
export const contract = c.router({
authenticate: {
changePassword: {
method: 'GET',
path: 'ping.view',
query: ssType._parameters.authenticate,
path: 'changePassword.view',
query: SubsonicApi.changePassword.parameters,
responses: {
200: ssType._response.authenticate,
200: SubsonicApi.changePassword.response,
},
},
createFavorite: {
createInternetRadioStation: {
method: 'GET',
path: 'star.view',
query: ssType._parameters.createFavorite,
path: 'createInternetRadioStation.view',
query: SubsonicApi.createInternetRadioStation.parameters,
responses: {
200: ssType._response.createFavorite,
200: SubsonicApi.createInternetRadioStation.response,
},
},
createPlaylist: {
method: 'GET',
path: 'createPlaylist.view',
query: SubsonicApi.createPlaylist.parameters,
responses: {
200: SubsonicApi.createPlaylist.response,
},
},
createShare: {
method: 'GET',
path: 'createShare.view',
query: SubsonicApi.createShare.parameters,
responses: {
200: SubsonicApi.createShare.response,
},
},
createUser: {
method: 'GET',
path: 'createUser.view',
query: SubsonicApi.createUser.parameters,
responses: {
200: SubsonicApi.createUser.response,
},
},
deleteInternetRadioStation: {
method: 'GET',
path: 'deleteInternetRadioStation.view',
query: SubsonicApi.deleteInternetRadioStation.parameters,
responses: {
200: SubsonicApi.deleteInternetRadioStation.response,
},
},
deletePlaylist: {
method: 'GET',
path: 'deletePlaylist.view',
query: SubsonicApi.deletePlaylist.parameters,
responses: {
200: SubsonicApi.deletePlaylist.response,
},
},
deleteShare: {
method: 'GET',
path: 'deleteShare.view',
query: SubsonicApi.deleteShare.parameters,
responses: {
200: SubsonicApi.deleteShare.response,
},
},
deleteUser: {
method: 'GET',
path: 'deleteUser.view',
query: SubsonicApi.deleteUser.parameters,
responses: {
200: SubsonicApi.deleteUser.response,
},
},
getAlbum: {
method: 'GET',
path: 'getAlbum.view',
query: SubsonicApi.getAlbum.parameters,
responses: {
200: SubsonicApi.getAlbum.response,
},
},
getAlbumInfo: {
method: 'GET',
path: 'getAlbumInfo.view',
query: SubsonicApi.getAlbumInfo.parameters,
responses: {
200: SubsonicApi.getAlbumInfo.response,
},
},
getAlbumInfo2: {
method: 'GET',
path: 'getAlbumInfo2.view',
query: SubsonicApi.getAlbumInfo2.parameters,
responses: {
200: SubsonicApi.getAlbumInfo2.response,
},
},
getAlbumList: {
method: 'GET',
path: 'getAlbumList.view',
query: SubsonicApi.getAlbumList.parameters,
responses: {
200: SubsonicApi.getAlbumList.response,
},
},
getAlbumList2: {
method: 'GET',
path: 'getAlbumList2.view',
query: SubsonicApi.getAlbumList2.parameters,
responses: {
200: SubsonicApi.getAlbumList2.response,
},
},
getArtist: {
method: 'GET',
path: 'getArtist.view',
query: SubsonicApi.getArtist.parameters,
responses: {
200: SubsonicApi.getArtist.response,
},
},
getArtistInfo: {
method: 'GET',
path: 'getArtistInfo.view',
query: ssType._parameters.artistInfo,
query: SubsonicApi.getArtistInfo.parameters,
responses: {
200: ssType._response.artistInfo,
200: SubsonicApi.getArtistInfo.response,
},
},
getMusicFolderList: {
getArtistInfo2: {
method: 'GET',
path: 'getArtistInfo2.view',
query: SubsonicApi.getArtistInfo2.parameters,
responses: {
200: SubsonicApi.getArtistInfo2.response,
},
},
getArtists: {
method: 'GET',
path: 'getArtists.view',
query: SubsonicApi.getArtists.parameters,
responses: {
200: SubsonicApi.getArtists.response,
},
},
getGenres: {
method: 'GET',
path: 'getGenres.view',
query: SubsonicApi.getGenres.parameters,
responses: {
200: SubsonicApi.getGenres.response,
},
},
getIndexes: {
method: 'GET',
path: 'getIndexes.view',
query: SubsonicApi.getIndexes.parameters,
responses: {
200: SubsonicApi.getIndexes.response,
},
},
getInternetRadioStations: {
method: 'GET',
path: 'getInternetRadioStations.view',
query: SubsonicApi.getInternetRadioStations.parameters,
responses: {
200: SubsonicApi.getInternetRadioStations.response,
},
},
getLicense: {
method: 'GET',
path: 'getLicense.view',
query: SubsonicApi.getLicense.parameters,
responses: {
200: SubsonicApi.getLicense.response,
},
},
getLyrics: {
method: 'GET',
path: 'getLyrics.view',
query: SubsonicApi.getLyrics.parameters,
responses: {
200: SubsonicApi.getLyrics.response,
},
},
getMusicDirectory: {
method: 'GET',
path: 'getMusicDirectory.view',
query: SubsonicApi.getMusicDirectory.parameters,
responses: {
200: SubsonicApi.getMusicDirectory.response,
},
},
getMusicFolders: {
method: 'GET',
path: 'getMusicFolders.view',
responses: {
200: ssType._response.musicFolderList,
200: SubsonicApi.getMusicFolders.response,
},
},
getRandomSongList: {
getNowPlaying: {
method: 'GET',
path: 'getNowPlaying.view',
query: SubsonicApi.getNowPlaying.parameters,
responses: {
200: SubsonicApi.getNowPlaying.response,
},
},
getOpenSubsonicExtensions: {
method: 'GET',
path: 'getOpenSubsonicExtensions.view',
query: SubsonicApi.getOpenSubsonicExtensions.parameters,
responses: {
200: SubsonicApi.getOpenSubsonicExtensions.response,
},
},
getPlaylist: {
method: 'GET',
path: 'getPlaylist.view',
query: SubsonicApi.getPlaylist.parameters,
responses: {
200: SubsonicApi.getPlaylist.response,
},
},
getPlaylists: {
method: 'GET',
path: 'getPlaylists.view',
query: SubsonicApi.getPlaylists.parameters,
responses: {
200: SubsonicApi.getPlaylists.response,
},
},
getRandomSongs: {
method: 'GET',
path: 'getRandomSongs.view',
query: ssType._parameters.randomSongList,
query: SubsonicApi.getRandomSongs.parameters,
responses: {
200: ssType._response.randomSongList,
200: SubsonicApi.getRandomSongs.response,
},
},
getTopSongsList: {
getScanStatus: {
method: 'GET',
path: 'getScanStatus.view',
responses: {
200: SubsonicApi.getScanStatus.response,
},
},
getShares: {
method: 'GET',
path: 'getShares.view',
query: SubsonicApi.getShares.parameters,
responses: {
200: SubsonicApi.getShares.response,
},
},
getSimilarSongs: {
method: 'GET',
path: 'getSimilarSongs.view',
query: SubsonicApi.getSimilarSongs.parameters,
responses: {
200: SubsonicApi.getSimilarSongs.response,
},
},
getSimilarSongs2: {
method: 'GET',
path: 'getSimilarSongs2.view',
query: SubsonicApi.getSimilarSongs2.parameters,
responses: {
200: SubsonicApi.getSimilarSongs2.response,
},
},
getSong: {
method: 'GET',
path: 'getSong.view',
query: SubsonicApi.getSong.parameters,
responses: {
200: SubsonicApi.getSong.response,
},
},
getSongsByGenre: {
method: 'GET',
path: 'getSongsByGenre.view',
query: SubsonicApi.getSongsByGenre.parameters,
responses: {
200: SubsonicApi.getSongsByGenre.response,
},
},
getStarred: {
method: 'GET',
path: 'getStarred.view',
query: SubsonicApi.getStarred.parameters,
responses: {
200: SubsonicApi.getStarred.response,
},
},
getStarred2: {
method: 'GET',
path: 'getStarred2.view',
query: SubsonicApi.getStarred2.parameters,
responses: {
200: SubsonicApi.getStarred2.response,
},
},
getTopSongs: {
method: 'GET',
path: 'getTopSongs.view',
query: ssType._parameters.topSongsList,
query: SubsonicApi.getTopSongs.parameters,
responses: {
200: ssType._response.topSongsList,
200: SubsonicApi.getTopSongs.response,
},
},
removeFavorite: {
getUser: {
method: 'GET',
path: 'unstar.view',
query: ssType._parameters.removeFavorite,
path: 'getUser.view',
query: SubsonicApi.getUser.parameters,
responses: {
200: ssType._response.removeFavorite,
200: SubsonicApi.getUser.response,
},
},
getUsers: {
method: 'GET',
path: 'getUsers.view',
query: SubsonicApi.getUsers.parameters,
responses: {
200: SubsonicApi.getUsers.response,
},
},
ping: {
method: 'GET',
path: 'ping.view',
query: SubsonicApi.ping.parameters,
responses: {
200: SubsonicApi.ping.response,
},
},
scrobble: {
method: 'GET',
path: 'scrobble.view',
query: ssType._parameters.scrobble,
query: SubsonicApi.scrobble.parameters,
responses: {
200: ssType._response.scrobble,
200: SubsonicApi.scrobble.response,
},
},
search3: {
method: 'GET',
path: 'search3.view',
query: ssType._parameters.search3,
query: SubsonicApi.search3.parameters,
responses: {
200: ssType._response.search3,
200: SubsonicApi.search3.response,
},
},
setRating: {
method: 'GET',
path: 'setRating.view',
query: ssType._parameters.setRating,
query: SubsonicApi.setRating.parameters,
responses: {
200: ssType._response.setRating,
200: SubsonicApi.setRating.response,
},
},
star: {
method: 'GET',
path: 'star.view',
query: SubsonicApi.star.parameters,
responses: {
200: SubsonicApi.star.response,
},
},
startScan: {
method: 'GET',
path: 'startScan.view',
responses: {
200: SubsonicApi.startScan.response,
},
},
unstar: {
method: 'GET',
path: 'unstar.view',
query: SubsonicApi.unstar.parameters,
responses: {
200: SubsonicApi.unstar.response,
},
},
updateInternetRadioStation: {
method: 'GET',
path: 'updateInternetRadioStation.view',
query: SubsonicApi.updateInternetRadioStation.parameters,
responses: {
200: SubsonicApi.updateInternetRadioStation.response,
},
},
updatePlaylist: {
method: 'GET',
path: 'updatePlaylist.view',
query: SubsonicApi.updatePlaylist.parameters,
responses: {
200: SubsonicApi.updatePlaylist.response,
},
},
updateShare: {
method: 'GET',
path: 'updateShare.view',
query: SubsonicApi.updateShare.parameters,
responses: {
200: SubsonicApi.updateShare.response,
},
},
updateUser: {
method: 'GET',
path: 'updateUser.view',
query: SubsonicApi.updateUser.parameters,
responses: {
200: SubsonicApi.updateUser.response,
},
},
});
@@ -102,14 +435,21 @@ axiosClient.interceptors.response.use(
(response) => {
const data = response.data;
if (data['subsonic-response'].status !== 'ok') {
// Ping endpoint returns a string
if (typeof data === 'string') {
return response;
}
if (data['subsonic-response']?.status !== 'ok') {
// Suppress code related to non-linked lastfm or spotify from Navidrome
if (data['subsonic-response'].error.code !== 0) {
if (data['subsonic-response']?.error.code !== 0) {
toast.error({
message: data['subsonic-response'].error.message,
message: data['subsonic-response']?.error.message,
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
});
}
return Promise.reject(data['subsonic-response']?.error);
}
return response;
@@ -131,7 +471,7 @@ const parsePath = (fullPath: string) => {
};
};
export const ssApiClient = (args: {
export const subsonicApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
@@ -162,9 +502,7 @@ export const ssApiClient = (args: {
}
try {
const result = await axiosClient.request<
z.infer<typeof ssType._response.baseResponse>
>({
const result = await axiosClient.request({
data: body,
headers,
method: method as Method,
@@ -180,14 +518,20 @@ export const ssApiClient = (args: {
});
return {
body: result.data['subsonic-response'],
headers: result.headers as any,
status: result.status,
body: result?.data,
headers: result?.headers as any,
status: result?.status,
};
} catch (e: Error | AxiosError | any) {
console.log('CATCH ERR');
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(
i18n.t('error.networkError', {
postProcess: 'sentenceCase',
}) as string,
);
}
const error = e as AxiosError;
const response = error.response as AxiosResponse;
File diff suppressed because it is too large Load Diff
+82 -11
View File
@@ -1,7 +1,15 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/types';
import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
import {
QueueSong,
LibraryItem,
AlbumArtist,
Album,
Genre,
MusicFolder,
Playlist,
} from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
const getCoverArtUrl = (args: {
@@ -27,16 +35,17 @@ const getCoverArtUrl = (args: {
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
item: z.infer<typeof SubsonicApi._baseTypes.song>,
server: ServerListItem | null,
deviceId: string,
size?: number,
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
size: size || 300,
}) || null;
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
@@ -105,15 +114,18 @@ const normalizeSong = (
};
const normalizeAlbumArtist = (
item: z.infer<typeof ssType._response.albumArtist>,
item:
| z.infer<typeof SubsonicApi._baseTypes.artist>
| z.infer<typeof SubsonicApi._baseTypes.artistListEntry>,
server: ServerListItem | null,
imageSize?: number,
): AlbumArtist => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
size: imageSize || 100,
}) || null;
return {
@@ -138,15 +150,18 @@ const normalizeAlbumArtist = (
};
const normalizeAlbum = (
item: z.infer<typeof ssType._response.album>,
item:
| z.infer<typeof SubsonicApi._baseTypes.album>
| z.infer<typeof SubsonicApi._baseTypes.albumListEntry>,
server: ServerListItem | null,
size?: number,
): Album => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 300,
size: size || 300,
}) || null;
return {
@@ -156,7 +171,7 @@ const normalizeAlbum = (
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
backdropImageUrl: null,
createdAt: item.created,
duration: item.duration,
duration: item.duration * 1000,
genres: item.genre
? [
{
@@ -181,7 +196,10 @@ const normalizeAlbum = (
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs: [],
songs:
(item as z.infer<typeof SubsonicApi._baseTypes.album>).song?.map((song) =>
normalizeSong(song, server, ''),
) || [],
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
@@ -189,8 +207,61 @@ const normalizeAlbum = (
};
};
export const ssNormalize = {
const normalizeGenre = (item: z.infer<typeof SubsonicApi._baseTypes.genre>): Genre => {
return {
albumCount: item.albumCount,
id: item.value,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.value,
songCount: item.songCount,
};
};
const normalizeMusicFolder = (
item: z.infer<typeof SubsonicApi._baseTypes.musicFolder>,
): MusicFolder => {
return {
id: item.id,
name: item.name,
};
};
const normalizePlaylist = (
item:
| z.infer<typeof SubsonicApi._baseTypes.playlist>
| z.infer<typeof SubsonicApi._baseTypes.playlistListEntry>,
server: ServerListItem | null,
): Playlist => {
return {
description: item.comment || null,
duration: item.duration,
genres: [],
id: item.id,
imagePlaceholderUrl: null,
imageUrl: getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 300,
}),
itemType: LibraryItem.PLAYLIST,
name: item.name,
owner: item.owner,
ownerId: item.owner,
public: item.public,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
};
};
export const subsonicNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
musicFolder: normalizeMusicFolder,
playlist: normalizePlaylist,
song: normalizeSong,
};
File diff suppressed because it is too large Load Diff
+72 -6
View File
@@ -124,7 +124,7 @@ export interface BasePaginatedResponse<T> {
error?: string | any;
items: T;
startIndex: number;
totalRecordCount: number;
totalRecordCount: number | null;
}
export type AuthenticationResponse = {
@@ -306,7 +306,9 @@ export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefine
export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
export enum GenreListSort {
ALBUM_COUNT = 'albumCount',
NAME = 'name',
SONG_COUNT = 'songCount',
}
export type GenreListQuery = {
@@ -330,10 +332,14 @@ type GenreListSortMap = {
export const genreListSortMap: GenreListSortMap = {
jellyfin: {
albumCount: undefined,
name: JFGenreListSort.NAME,
songCount: undefined,
},
navidrome: {
albumCount: undefined,
name: NDGenreListSort.NAME,
songCount: undefined,
},
subsonic: {
name: undefined,
@@ -370,7 +376,12 @@ export type AlbumListQuery = {
navidrome?: Partial<z.infer<typeof ndType._parameters.albumList>>;
};
artistIds?: string[];
genre?: string;
isCompilation?: boolean;
isFavorite?: boolean;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: AlbumListSort;
@@ -481,8 +492,13 @@ export type SongListQuery = {
};
albumIds?: string[];
artistIds?: string[];
genre?: string;
genreId?: string;
imageSize?: number;
isFavorite?: boolean;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: SongListSort;
@@ -802,6 +818,7 @@ export type CreatePlaylistBody = {
};
comment?: string;
name: string;
public?: boolean;
};
export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs;
@@ -826,6 +843,11 @@ export type UpdatePlaylistBody = {
comment?: string;
genres?: Genre[];
name: string;
owner?: string;
ownerId?: string;
public?: boolean;
rules?: Record<string, any>;
sync?: boolean;
};
export type UpdatePlaylistArgs = {
@@ -917,10 +939,9 @@ export type PlaylistSongListResponse = BasePaginatedResponse<Song[]> | null | un
export type PlaylistSongListQuery = {
id: string;
limit?: number;
sortBy?: SongListSort;
sortOrder?: SortOrder;
startIndex: number;
searchTerm?: string;
sortBy: SongListSort;
sortOrder: SortOrder;
};
export type PlaylistSongListArgs = { query: PlaylistSongListQuery } & BaseEndpointArgs;
@@ -1014,7 +1035,7 @@ export type SearchQuery = {
albumLimit?: number;
albumStartIndex?: number;
musicFolderId?: string;
query?: string;
query: string;
songLimit?: number;
songStartIndex?: number;
};
@@ -1139,3 +1160,48 @@ export type FontData = {
postscriptName: string;
style: string;
};
export type ControllerEndpoint = Partial<{
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { password: string; username: string },
) => Promise<AuthenticationResponse>;
clearPlaylist: () => void;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
getAlbumSongList: (args: AlbumDetailArgs) => Promise<SongListResponse>; // TODO
getArtistDetail: () => void;
getArtistInfo: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getSongListCount: (args: SongListArgs) => Promise<number>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
}>;
+188 -1
View File
@@ -1,8 +1,20 @@
import { AxiosHeaders } from 'axios';
import { z } from 'zod';
import { toast } from '/@/renderer/components';
import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types';
import {
Album,
AlbumArtist,
AlbumArtistListSort,
AlbumListSort,
QueueSong,
SongListSort,
SortOrder,
} from '/@/renderer/api/types';
import orderBy from 'lodash/orderBy';
import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle';
import { z } from 'zod';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@@ -38,3 +50,178 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
useAuthStore.getState().actions.setCurrentServer(null);
}
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
let results = albums;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
default:
break;
}
return results;
};
export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder: SortOrder) => {
let results = songs;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0].name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['created'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort,
sortOrder: SortOrder,
) => {
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
+1 -1
View File
@@ -275,7 +275,7 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
name: {
property: 'name',
route: {
route: AppRoute.PLAYLISTS_DETAIL,
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
},
},
@@ -34,6 +34,7 @@ interface UseAgGridProps<TFilter> {
columnType?: 'albumDetail' | 'generic';
contextMenu: SetContextMenuItems;
customFilters?: Partial<TFilter>;
isClientSide?: boolean;
isClientSideSort?: boolean;
isSearchParams?: boolean;
itemCount?: number;
@@ -43,6 +44,8 @@ interface UseAgGridProps<TFilter> {
tableRef: MutableRefObject<AgGridReactType | null>;
}
const BLOCK_SIZE = 500;
export const useVirtualTable = <TFilter>({
server,
tableRef,
@@ -52,6 +55,7 @@ export const useVirtualTable = <TFilter>({
itemCount,
customFilters,
isSearchParams,
isClientSide,
isClientSideSort,
columnType,
}: UseAgGridProps<TFilter>) => {
@@ -182,6 +186,19 @@ export const useVirtualTable = <TFilter>({
return;
}
if (results.totalRecordCount === null) {
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
const lastRowIndex = hasMoreRows
? undefined
: (properties.filter.offset || 0) + results.items.length;
params.successCallback(
results?.items || [],
hasMoreRows ? undefined : lastRowIndex,
);
return;
}
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
},
rowCount: undefined,
@@ -321,6 +338,7 @@ export const useVirtualTable = <TFilter>({
alwaysShowHorizontalScroll: true,
autoFitColumns: properties.table.autoFit,
blockLoadDebounceMillis: 200,
cacheBlockSize: 500,
getRowId: (data: GetRowIdParams<any>) => data.data.id,
infiniteInitialRowCount: itemCount || 100,
pagination: isPaginationEnabled,
@@ -335,10 +353,11 @@ export const useVirtualTable = <TFilter>({
: undefined,
rowBuffer: 20,
rowHeight: properties.table.rowHeight || 40,
rowModelType: 'infinite' as RowModelType,
rowModelType: isClientSide ? 'clientSide' : ('infinite' as RowModelType),
suppressRowDrag: true,
};
}, [
isClientSide,
isPaginationEnabled,
isSearchParams,
itemCount,
@@ -370,7 +389,9 @@ export const useVirtualTable = <TFilter>({
);
break;
case LibraryItem.PLAYLIST:
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id }));
navigate(
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }),
);
break;
default:
break;
@@ -22,6 +22,7 @@ import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
@@ -139,14 +140,61 @@ const FILTERS = {
value: AlbumListSort.YEAR,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR,
},
],
};
interface AlbumListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount: number | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
export const AlbumListHeaderFilters = ({
gridRef,
tableRef,
itemCount,
}: AlbumListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { pageKey, customFilters, handlePlay } = useListContext();
@@ -159,6 +207,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
const cq = useContainerQuery();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.ALBUM,
server,
});
@@ -185,27 +234,35 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
);
const handleOpenFiltersModal = () => {
let FilterComponent;
switch (server?.type) {
case ServerType.NAVIDROME:
FilterComponent = NavidromeAlbumFilters;
break;
case ServerType.JELLYFIN:
FilterComponent = JellyfinAlbumFilters;
break;
case ServerType.SUBSONIC:
FilterComponent = SubsonicAlbumFilters;
break;
default:
break;
}
if (!FilterComponent) {
return;
}
openModal({
children: (
<>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeAlbumFilters
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
) : (
<JellyfinAlbumFilters
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
)}
</>
<FilterComponent
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
),
title: 'Album Filters',
});
@@ -341,8 +398,20 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
filter?._custom?.jellyfin &&
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
const isSubsonicFilterApplied =
server?.type === ServerType.SUBSONIC &&
(filter.maxYear || filter.minYear || filter.genre || filter.isFavorite);
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
}, [
filter?._custom?.jellyfin,
filter?._custom?.navidrome,
filter.genre,
filter.isFavorite,
filter.maxYear,
filter.minYear,
server?.type,
]);
const isFolderFilterApplied = useMemo(() => {
return filter.musicFolderId !== undefined;
@@ -38,6 +38,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
const playButtonBehavior = usePlayButtonBehavior();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.ALBUM,
server,
});
@@ -94,6 +95,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
<FilterBar>
<AlbumListHeaderFilters
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</FilterBar>
@@ -0,0 +1,143 @@
import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
import { useGenreList } from '/@/renderer/features/genres';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
interface SubsonicAlbumFiltersProps {
onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string;
serverId?: string;
}
export const SubsonicAlbumFilters = ({
onFilterChange,
pageKey,
serverId,
}: SubsonicAlbumFiltersProps) => {
const { t } = useTranslation();
const { filter } = useListStoreByKey({ key: pageKey });
const { setFilter } = useListStoreActions();
const genreListQuery = useGenreList({
query: {
sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId,
});
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
label: genre.name,
value: genre.id,
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilter({
data: {
genre: e || undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
}, 250);
const toggleFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
data: {
isFavorite: e.target.checked ? true : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter.isFavorite,
},
];
const handleYearFilter = debounce((e: number | string, type: 'min' | 'max') => {
let data = {};
if (type === 'min') {
data = {
minYear: e || undefined,
};
} else {
data = {
maxYear: e || undefined,
};
}
console.log('data', data);
const updatedFilters = setFilter({
data,
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
}, 500);
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
<Group
key={`nd-filter-${filter.label}`}
position="apart"
>
<Text>{filter.label}</Text>
<Switch
checked={filter?.value || false}
onChange={filter.onChange}
/>
</Group>
))}
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter.minYear}
disabled={filter.genre}
hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={5000}
min={0}
onChange={(e) => handleYearFilter(e, 'min')}
/>
<NumberInput
defaultValue={filter.maxYear}
disabled={filter.genre}
hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={5000}
min={0}
onChange={(e) => handleYearFilter(e, 'max')}
/>
</Group>
<Group grow>
<Select
clearable
searchable
data={genreList}
defaultValue={filter.genre}
disabled={filter.minYear || filter.maxYear}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
/>
</Group>
</Stack>
);
};
@@ -0,0 +1,44 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import type { AlbumListQuery } from '/@/renderer/api/types';
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const getAlbumListCountQuery = (query: AlbumListQuery) => {
const filter: Record<string, unknown> = {};
if (query.artistIds) filter.artistIds = query.artistIds;
if (query.maxYear) filter.maxYear = query.maxYear;
if (query.minYear) filter.minYear = query.minYear;
if (query.searchTerm) filter.searchTerm = query.searchTerm;
if (query.genre) filter.genre = query.genre;
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
if (query.isCompilation) filter.isCompilation = query.isCompilation;
if (query.isFavorite) filter.isCompilation = query.isFavorite;
if (Object.keys(filter).length === 0) return undefined;
return filter;
};
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albums.count(serverId || '', getAlbumListCountQuery(query)),
...options,
});
};
@@ -9,12 +9,12 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
import { useAlbumListCount } from '/@/renderer/features/albums/queries/album-list-count-query';
const AlbumListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
@@ -42,23 +42,18 @@ const AlbumListRoute = () => {
key: pageKey,
});
const itemCountCheck = useAlbumList({
const itemCountCheck = useAlbumListCount({
options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1,
startIndex: 0,
...albumListFilter,
},
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const handlePlay = useCallback(
async (args: { initialSongId?: string; playType: Play }) => {
@@ -100,6 +100,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
: undefined),
},
},
artistIds: [albumArtistId],
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
@@ -122,6 +123,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
: undefined),
},
},
artistIds: [albumArtistId],
isCompilation: true,
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
@@ -85,6 +85,28 @@ const FILTERS = {
value: AlbumArtistListSort.SONG_COUNT,
},
],
subsonic: [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.ALBUM_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.FAVORITED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RATING,
},
],
};
interface AlbumArtistListHeaderFiltersProps {
@@ -35,6 +35,7 @@ export const AlbumArtistListHeader = ({
const cq = useContainerQuery();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.ALBUM_ARTIST,
server,
});
@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListQuery } from '/@/renderer/api/types';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const getAlbumArtistListCountQuery = (query: AlbumArtistListQuery) => {
const filter: Record<string, unknown> = {};
if (query.searchTerm) filter.searchTerm = query.searchTerm;
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
if (Object.keys(filter).length === 0) return undefined;
return filter;
};
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albumArtists.count(serverId || '', getAlbumArtistListCountQuery(query)),
...options,
});
};
@@ -7,7 +7,7 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useAlbumArtistListCount } from '/@/renderer/features/artists/queries/album-artist-list-count-query';
import { AnimatedPage } from '/@/renderer/features/shared';
const AlbumArtistListRoute = () => {
@@ -18,23 +18,18 @@ const AlbumArtistListRoute = () => {
const albumArtistListFilter = useListFilterByKey({ key: pageKey });
const itemCountCheck = useAlbumArtistList({
const itemCountCheck = useAlbumArtistListCount({
options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1,
startIndex: 0,
...albumArtistListFilter,
},
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const providerValue = useMemo(() => {
return {
@@ -40,7 +40,7 @@ export const useDiscordRpc = () => {
largeImageText: currentSong?.album || 'Unknown album',
smallImageKey: undefined,
smallImageText: currentStatus,
state: artists && `By ${artists}`,
state: artists && `By ${artists}` || "Unknown artist",
};
if (currentStatus === PlayerStatus.PLAYING) {
@@ -37,14 +37,36 @@ const FILTERS = {
value: GenreListSort.NAME,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: GenreListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: GenreListSort.ALBUM_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: GenreListSort.SONG_COUNT,
},
],
};
interface GenreListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount: number | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFiltersProps) => {
export const GenreListHeaderFilters = ({
gridRef,
tableRef,
itemCount,
}: GenreListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { pageKey, customFilters } = useListContext();
@@ -54,6 +76,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
const cq = useContainerQuery();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.GENRE,
server,
});
@@ -34,6 +34,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
const { setFilter, setTablePagination } = useListStoreActions();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.GENRE,
server,
});
@@ -89,6 +90,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
<FilterBar>
<GenreListHeaderFilters
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</FilterBar>
+3 -2
View File
@@ -23,7 +23,6 @@ export const getPlaylistSongsById = async (args: {
id,
sortBy: SongListSort.ID,
sortOrder: SortOrder.ASC,
startIndex: 0,
...query,
};
@@ -139,7 +138,9 @@ export const getGenreSongsById = async (args: {
);
data.items.push(...res!.items);
data.totalRecordCount += res!.totalRecordCount;
if (data.totalRecordCount) {
data.totalRecordCount += res!.totalRecordCount || 0;
}
}
return data;
@@ -136,7 +136,6 @@ export const AddToPlaylistContextModal = ({
if (values.skipDuplicates) {
const query = {
id: playlistId,
startIndex: 0,
};
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
@@ -151,7 +150,11 @@ export const AddToPlaylistContextModal = ({
server,
signal,
},
query: { id: playlistId, startIndex: 0 },
query: {
id: playlistId,
sortBy: SongListSort.ID,
sortOrder: SortOrder.ASC,
},
});
});
@@ -32,6 +32,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
},
comment: '',
name: '',
public: false,
},
});
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
@@ -86,7 +87,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
);
});
const isPublicDisplayed = server?.type === ServerType.NAVIDROME;
const isPublicDisplayed =
server?.type === ServerType.NAVIDROME || server?.type === ServerType.SUBSONIC;
const isSubmitDisabled = !form.values.name || mutation.isLoading;
return (
@@ -115,7 +117,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
context: 'public',
postProcess: 'titleCase',
})}
{...form.getInputProps('_custom.navidrome.public', {
{...form.getInputProps('public', {
type: 'checkbox',
})}
/>
@@ -1,254 +0,0 @@
import { MutableRefObject, useMemo, useRef } from 'react';
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { useTranslation } from 'react-i18next';
import { RiMoreFill } from 'react-icons/ri';
import { generatePath, useNavigate, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { useListStoreByKey } from '../../../store/list.store';
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import {
PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { usePlaylistSongListInfinite } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
import { PlayButton, PLAY_TYPES } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
const ContentContainer = styled.div`
position: relative;
display: flex;
flex-direction: column;
padding: 1rem 2rem 5rem;
overflow: hidden;
.ag-theme-alpine-dark {
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
}
`;
interface PlaylistDetailContentProps {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { playlistId } = useParams() as { playlistId: string };
const { table } = useListStoreByKey({ key: LibraryItem.SONG });
const handlePlayQueueAdd = usePlayQueueAdd();
const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const playButtonBehavior = usePlayButtonBehavior();
const playlistSongsQueryInfinite = usePlaylistSongListInfinite({
options: {
cacheTime: 0,
keepPreviousData: false,
},
query: {
id: playlistId,
limit: 50,
startIndex: 0,
},
serverId: server?.id,
});
const handleLoadMore = () => {
playlistSongsQueryInfinite.fetchNextPage();
};
const columnDefs: ColDef[] = useMemo(
() =>
getColumnDefs(table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'),
[table.columns],
);
const contextMenuItems = useMemo(() => {
if (detailQuery?.data?.rules) {
return SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}
return PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}, [detailQuery?.data?.rules]);
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, contextMenuItems, {
playlistId,
});
const playlistSongData = useMemo(
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p?.items),
[playlistSongsQueryInfinite.data?.pages],
);
const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = () => {
deletePlaylistMutation.mutate(
{ query: { id: playlistId }, serverId: server?.id },
{
onError: (err) => {
toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }),
});
},
onSuccess: () => {
closeAllModals();
navigate(AppRoute.PLAYLISTS);
},
},
);
};
const openDeletePlaylist = () => {
openModal({
children: (
<ConfirmModal
loading={deletePlaylistMutation.isLoading}
onConfirm={handleDeletePlaylist}
>
Are you sure you want to delete this playlist?
</ConfirmModal>
),
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
});
};
const handlePlay = (playType?: Play) => {
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
playType: playType || playButtonBehavior,
});
};
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
const loadMoreRef = useRef<HTMLButtonElement | null>(null);
return (
<ContentContainer>
<Group
p="1rem"
position="apart"
>
<Group>
<PlayButton onClick={() => handlePlay()} />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
variant="subtle"
>
<RiMoreFill size={20} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{PLAY_TYPES.filter((type) => type.play !== playButtonBehavior).map(
(type) => (
<DropdownMenu.Item
key={`playtype-${type.play}`}
onClick={() => handlePlay(type.play)}
>
{type.label}
</DropdownMenu.Item>
),
)}
<DropdownMenu.Divider />
<DropdownMenu.Item
onClick={() => {
if (!detailQuery.data || !server) return;
openUpdatePlaylistModal({ playlist: detailQuery.data, server });
}}
>
Edit playlist
</DropdownMenu.Item>
<DropdownMenu.Item onClick={openDeletePlaylist}>
Delete playlist
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
<Button
compact
uppercase
component={Link}
to={generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId })}
variant="subtle"
>
View full playlist
</Button>
</Group>
</Group>
<Box>
<VirtualTable
ref={tableRef}
autoFitColumns
autoHeight
deselectOnClickOutside
stickyHeader
suppressCellFocus
suppressHorizontalScroll
suppressLoadingOverlay
suppressRowDrag
columnDefs={columnDefs}
getRowId={(data) => {
// It's possible that there are duplicate song ids in a playlist
return `${data.data.id}-${data.data.pageIndex}`;
}}
rowClassRules={rowClassRules}
rowData={playlistSongData}
rowHeight={60}
rowSelection="multiple"
onCellContextMenu={handleContextMenu}
onRowDoubleClicked={handleRowDoubleClick}
/>
</Box>
<MotionGroup
p="2rem"
position="center"
onViewportEnter={handleLoadMore}
>
<Button
ref={loadMoreRef}
compact
disabled={!playlistSongsQueryInfinite.hasNextPage}
loading={playlistSongsQueryInfinite.isFetchingNextPage}
variant="subtle"
onClick={handleLoadMore}
>
{playlistSongsQueryInfinite.hasNextPage ? 'Load more' : 'End of playlist'}
</Button>
</MotionGroup>
</ContentContainer>
);
};
@@ -1,79 +0,0 @@
import { forwardRef, Fragment, Ref } from 'react';
import { Group, Stack } from '@mantine/core';
import { useParams } from 'react-router';
import { Badge, Text } from '/@/renderer/components';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { LibraryHeader } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';
import { formatDurationString } from '/@/renderer/utils';
import { LibraryItem } from '/@/renderer/api/types';
import { useCurrentServer } from '../../../store/auth.store';
interface PlaylistDetailHeaderProps {
background: string;
imagePlaceholderUrl?: string | null;
imageUrl?: string | null;
}
export const PlaylistDetailHeader = forwardRef(
(
{ background, imageUrl, imagePlaceholderUrl }: PlaylistDetailHeaderProps,
ref: Ref<HTMLDivElement>,
) => {
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const metadataItems = [
{
id: 'songCount',
secondary: false,
value: `${detailQuery?.data?.songCount || 0} songs`,
},
{
id: 'duration',
secondary: true,
value:
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
},
];
const isSmartPlaylist = detailQuery?.data?.rules;
return (
<Stack>
<LibraryHeader
ref={ref}
background={background}
imagePlaceholderUrl={imagePlaceholderUrl}
imageUrl={imageUrl}
item={{ route: AppRoute.PLAYLISTS, type: LibraryItem.PLAYLIST }}
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group spacing="sm">
{metadataItems.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text $noSelect></Text>}
<Text $secondary={item.secondary}>{item.value}</Text>
</Fragment>
))}
{isSmartPlaylist && (
<>
<Text $noSelect></Text>
<Badge
radius="sm"
size="md"
>
Smart Playlist
</Badge>
</>
)}
</Group>
<Text lineClamp={3}>{detailQuery?.data?.description}</Text>
</Stack>
</LibraryHeader>
</Stack>
);
},
);
@@ -2,25 +2,15 @@ import type {
BodyScrollEvent,
ColDef,
GridReadyEvent,
IDatasource,
PaginationChangedEvent,
RowDoubleClickedEvent,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import debounce from 'lodash/debounce';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { useParams } from 'react-router';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
LibraryItem,
PlaylistSongListQuery,
QueueSong,
SongListSort,
SortOrder,
} from '/@/renderer/api/types';
import { LibraryItem, QueueSong, Song } from '/@/renderer/api/types';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { TablePagination, VirtualTable, getColumnDefs } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
@@ -31,7 +21,7 @@ import {
} from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
useCurrentSong,
@@ -43,26 +33,19 @@ import {
} from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ListDisplayType } from '/@/renderer/types';
import { useAppFocus } from '/@/renderer/hooks';
interface PlaylistDetailContentProps {
songs: Song[];
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailContentProps) => {
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
const { playlistId } = useParams() as { playlistId: string };
const queryClient = useQueryClient();
const status = useCurrentStatus();
const isFocused = useAppFocus();
const currentSong = useCurrentSong();
const server = useCurrentServer();
const page = usePlaylistDetailStore();
const filters: Partial<PlaylistSongListQuery> = useMemo(() => {
return {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
};
}, [page?.table.id, playlistId]);
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
@@ -82,15 +65,6 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
const checkPlaylistList = usePlaylistSongList({
query: {
id: playlistId,
limit: 1,
startIndex: 0,
},
serverId: server?.id,
});
const columnDefs: ColDef[] = useMemo(
() => getColumnDefs(page.table.columns, false, 'generic'),
[page.table.columns],
@@ -98,44 +72,9 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
const onGridReady = useCallback(
(params: GridReadyEvent) => {
const dataSource: IDatasource = {
getRows: async (params) => {
const limit = params.endRow - params.startRow;
const startIndex = params.startRow;
const query: PlaylistSongListQuery = {
id: playlistId,
limit,
startIndex,
...filters,
};
const queryKey = queryKeys.playlists.songList(
server?.id || '',
playlistId,
query,
);
if (!server) return;
const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
api.controller.getPlaylistSongList({
apiClientProps: {
server,
signal,
},
query,
}),
);
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
},
rowCount: undefined,
};
params.api.setDatasource(dataSource);
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
},
[filters, pagination.scrollOffset, playlistId, queryClient, server],
[pagination.scrollOffset],
);
const handleGridSizeChange = () => {
@@ -249,13 +188,13 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
status,
}}
getRowId={(data) => data.data.uniqueId}
infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100}
pagination={isPaginationEnabled}
paginationAutoPageSize={isPaginationEnabled}
paginationPageSize={pagination.itemsPerPage || 100}
rowClassRules={rowClassRules}
rowData={songs}
rowHeight={page.table.rowHeight || 40}
rowModelType="infinite"
rowModelType="clientSide"
onBodyScrollEnd={handleScroll}
onCellContextMenu={handleContextMenu}
onColumnMoved={handleColumnChange}
@@ -1,53 +1,50 @@
import { useCallback, ChangeEvent, MutableRefObject, MouseEvent } from 'react';
import { IDatasource } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query';
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
RiMoreFill,
RiSettings3Fill,
RiPlayFill,
RiAddCircleFill,
RiAddBoxFill,
RiEditFill,
RiAddCircleFill,
RiDeleteBinFill,
RiEditFill,
RiMoreFill,
RiPlayFill,
RiRefreshLine,
RiSettings3Fill,
} from 'react-icons/ri';
import { api } from '/@/renderer/api';
import { useNavigate, useParams } from 'react-router';
import i18n from '/@/i18n/i18n';
import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
import {
DropdownMenu,
Button,
Slider,
ConfirmModal,
DropdownMenu,
MultiSelect,
Slider,
Switch,
Text,
ConfirmModal,
toast,
} from '/@/renderer/components';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { OrderToggleButton } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import {
useCurrentServer,
SongListFilter,
usePlaylistDetailStore,
useSetPlaylistDetailFilters,
useSetPlaylistDetailTable,
useSetPlaylistStore,
useSetPlaylistTablePagination,
} from '/@/renderer/store';
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { useParams, useNavigate } from 'react-router';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { AppRoute } from '/@/renderer/router/routes';
import { OrderToggleButton } from '/@/renderer/features/shared';
import i18n from '/@/i18n/i18n';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
const FILTERS = {
jellyfin: [
@@ -150,7 +147,7 @@ const FILTERS = {
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
value: SongListSort.GENRE,
},
{
@@ -184,6 +181,68 @@ const FILTERS = {
value: SongListSort.YEAR,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
value: SongListSort.ID,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: SongListSort.ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: SongListSort.DURATION,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: SongListSort.FAVORITED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
value: SongListSort.GENRE,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: SongListSort.RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: SongListSort.YEAR,
},
],
};
interface PlaylistDetailSongListHeaderFiltersProps {
@@ -228,56 +287,18 @@ export const PlaylistDetailSongListHeaderFilters = ({
setTable({ rowHeight: e });
};
const handleFilterChange = useCallback(
async (filters: SongListFilter) => {
const dataSource: IDatasource = {
getRows: async (params) => {
const limit = params.endRow - params.startRow;
const startIndex = params.startRow;
const handleFilterChange = useCallback(async () => {
tableRef.current?.api.redrawRows();
tableRef.current?.api.ensureIndexVisible(0, 'top');
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, {
id: playlistId,
limit,
startIndex,
...filters,
});
const songsRes = await queryClient.fetchQuery(
queryKey,
async ({ signal }) =>
api.controller.getPlaylistSongList({
apiClientProps: {
server,
signal,
},
query: {
id: playlistId,
limit,
startIndex,
...filters,
},
}),
{ cacheTime: 1000 * 60 * 1 },
);
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
},
rowCount: undefined,
};
tableRef.current?.api.setDatasource(dataSource);
tableRef.current?.api.purgeInfiniteCache();
tableRef.current?.api.ensureIndexVisible(0, 'top');
if (page.display === ListDisplayType.TABLE_PAGINATED) {
setPagination({ data: { currentPage: 0 } });
}
},
[tableRef, page.display, server, playlistId, queryClient, setPagination],
);
if (page.display === ListDisplayType.TABLE_PAGINATED) {
setPagination({ data: { currentPage: 0 } });
}
}, [tableRef, page.display, setPagination]);
const handleRefresh = () => {
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
handleFilterChange({ ...page?.table.id[playlistId].filter, ...filters });
handleFilterChange();
};
const handleSetSortBy = useCallback(
@@ -288,20 +309,20 @@ export const PlaylistDetailSongListHeaderFilters = ({
(f) => f.value === e.currentTarget.value,
)?.defaultOrder;
const updatedFilters = setFilter(playlistId, {
setFilter(playlistId, {
sortBy: e.currentTarget.value as SongListSort,
sortOrder: sortOrder || SortOrder.ASC,
});
handleFilterChange(updatedFilters);
handleFilterChange();
},
[handleFilterChange, playlistId, server?.type, setFilter],
);
const handleToggleSortOrder = useCallback(() => {
const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
const updatedFilters = setFilter(playlistId, { sortOrder: newSortOrder });
handleFilterChange(updatedFilters);
setFilter(playlistId, { sortOrder: newSortOrder });
handleFilterChange();
}, [filters.sortOrder, handleFilterChange, playlistId, setFilter]);
const handleSetViewType = useCallback(
@@ -1,6 +1,6 @@
import { MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Stack } from '@mantine/core';
import { Flex, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { LibraryItem } from '/@/renderer/api/types';
@@ -45,23 +45,30 @@ export const PlaylistDetailSongListHeader = ({
return (
<Stack spacing={0}>
<PageHeader backgroundColor="var(--titlebar-bg)">
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
<Paper
fw="600"
px="1rem"
py="0.3rem"
radius="sm"
>
{itemCount === null || itemCount === undefined ? (
<SpinnerIcon />
) : (
itemCount
)}
</Paper>
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
</LibraryHeaderBar>
<Flex
justify="space-between"
w="100%"
>
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton
onClick={() => handlePlay(playButtonBehavior)}
/>
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
<Paper
fw="600"
px="1rem"
py="0.3rem"
radius="sm"
>
{itemCount === null || itemCount === undefined ? (
<SpinnerIcon />
) : (
itemCount
)}
</Paper>
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
</LibraryHeaderBar>
</Flex>
</PageHeader>
<Paper p="1rem">
<PlaylistDetailSongListHeaderFilters
@@ -1,5 +1,5 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { useListContext } from '../../../context/list-context';
@@ -22,7 +22,7 @@ import {
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useGeneralSettings, useListStoreByKey } from '/@/renderer/store';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { CardRow, ListDisplayType } from '/@/renderer/types';
interface PlaylistListGridViewProps {
@@ -37,7 +37,6 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
const handlePlayQueueAdd = usePlayQueueAdd();
const { display, grid, filter } = useListStoreByKey({ key: pageKey });
const { setGrid } = useListStoreActions();
const { defaultFullPlaylist } = useGeneralSettings();
const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite({});
@@ -68,9 +67,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
};
const cardRows = useMemo(() => {
const rows: CardRow<Playlist>[] = defaultFullPlaylist
? [PLAYLIST_CARD_ROWS.nameFull]
: [PLAYLIST_CARD_ROWS.name];
const rows: CardRow<Playlist>[] = [PLAYLIST_CARD_ROWS.name];
switch (filter.sortBy) {
case PlaylistListSort.DURATION:
@@ -93,7 +90,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
}
return rows;
}, [defaultFullPlaylist, filter.sortBy]);
}, [filter.sortBy]);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
@@ -187,9 +184,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
route={{
route: defaultFullPlaylist
? AppRoute.PLAYLISTS_DETAIL_SONGS
: AppRoute.PLAYLISTS_DETAIL,
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
}}
width={width}
@@ -69,6 +69,38 @@ const FILTERS = {
value: PlaylistListSort.UPDATED_AT,
},
],
subsonic: [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: PlaylistListSort.DURATION,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: PlaylistListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.owner', { postProcess: 'titleCase' }),
value: PlaylistListSort.OWNER,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isPublic', { postProcess: 'titleCase' }),
value: PlaylistListSort.PUBLIC,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: PlaylistListSort.SONG_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyUpdated', { postProcess: 'titleCase' }),
value: PlaylistListSort.UPDATED_AT,
},
],
};
interface PlaylistListHeaderFiltersProps {
@@ -44,6 +44,7 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
};
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.PLAYLIST,
server,
});
@@ -8,7 +8,7 @@ import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { useCurrentServer } from '/@/renderer/store';
interface PlaylistListTableViewProps {
itemCount?: number;
@@ -18,16 +18,11 @@ interface PlaylistListTableViewProps {
export const PlaylistListTableView = ({ tableRef, itemCount }: PlaylistListTableViewProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const { defaultFullPlaylist } = useGeneralSettings();
const pageKey = 'playlist';
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
if (!e.data) return;
if (defaultFullPlaylist) {
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
} else {
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id }));
}
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
};
const tableProps = useVirtualTable({
@@ -1,9 +1,9 @@
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import type { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/renderer/api/types';
import type { PlaylistSongListQuery } from '/@/renderer/api/types';
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
import { api } from '/@/renderer/api';
export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>) => {
const { options, query, serverId } = args || {};
@@ -23,31 +23,31 @@ export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>)
});
};
export const usePlaylistSongListInfinite = (args: QueryHookArgs<PlaylistSongListQuery>) => {
const { options, query, serverId } = args || {};
const server = getServerById(serverId);
// export const usePlaylistSongListInfinite = (args: QueryHookArgs<PlaylistSongListQuery>) => {
// const { options, query, serverId } = args || {};
// const server = getServerById(serverId);
return useInfiniteQuery({
enabled: !!server,
getNextPageParam: (lastPage: PlaylistSongListResponse | undefined, pages) => {
if (!lastPage?.items) return undefined;
if (lastPage?.items?.length >= (query?.limit || 50)) {
return pages?.length;
}
// return useInfiniteQuery({
// enabled: !!server,
// getNextPageParam: (lastPage: PlaylistSongListResponse | undefined, pages) => {
// if (!lastPage?.items) return undefined;
// if (lastPage?.items?.length >= (query?.limit || 50)) {
// return pages?.length;
// }
return undefined;
},
queryFn: ({ pageParam = 0, signal }) => {
return api.controller.getPlaylistSongList({
apiClientProps: { server, signal },
query: {
...query,
limit: query.limit || 50,
startIndex: pageParam * (query.limit || 50),
},
});
},
queryKey: queryKeys.playlists.detailSongList(server?.id || '', query.id, query),
...options,
});
};
// return undefined;
// },
// queryFn: ({ pageParam = 0, signal }) => {
// return api.controller.getPlaylistSongList({
// apiClientProps: { server, signal },
// query: {
// ...query,
// limit: query.limit || 50,
// startIndex: pageParam * (query.limit || 50),
// },
// });
// },
// queryKey: queryKeys.playlists.detailSongList(server?.id || '', query.id, query),
// ...options,
// });
// };
@@ -1,77 +0,0 @@
import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useParams } from 'react-router';
import { LibraryItem } from '/@/renderer/api/types';
import { NativeScrollArea, Spinner } from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { PlaylistDetailContent } from '/@/renderer/features/playlists/components/playlist-detail-content';
import { PlaylistDetailHeader } from '/@/renderer/features/playlists/components/playlist-detail-header';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useFastAverageColor } from '/@/renderer/hooks';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { useCurrentServer } from '../../../store/auth.store';
const PlaylistDetailRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const { color: background, colorId } = useFastAverageColor({
algorithm: 'sqrt',
id: playlistId,
src: detailQuery?.data?.imageUrl,
srcLoaded: !detailQuery?.isLoading,
});
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = () => {
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
playType: playButtonBehavior,
});
};
if (!background || colorId !== playlistId) {
return <Spinner container />;
}
return (
<AnimatedPage key={`playlist-detail-${playlistId}`}>
<NativeScrollArea
ref={scrollAreaRef}
pageHeaderProps={{
backgroundColor: background,
children: (
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
<LibraryHeaderBar.Title>
{detailQuery?.data?.name}
</LibraryHeaderBar.Title>
</LibraryHeaderBar>
),
offset: 200,
target: headerRef,
}}
>
<PlaylistDetailHeader
ref={headerRef}
background={background}
imagePlaceholderUrl={detailQuery?.data?.imageUrl}
imageUrl={detailQuery?.data?.imageUrl}
/>
<PlaylistDetailContent tableRef={tableRef} />
</NativeScrollArea>
</AnimatedPage>
);
};
export default PlaylistDetailRoute;
@@ -139,28 +139,20 @@ const PlaylistDetailSongListRoute = () => {
const page = usePlaylistDetailStore();
const filters: Partial<PlaylistSongListQuery> = {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
sortBy: page?.table.id[playlistId]?.filter?.sortBy,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder,
};
const itemCountCheck = usePlaylistSongList({
options: {
cacheTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
const { data } = usePlaylistSongList({
query: {
id: playlistId,
limit: 1,
startIndex: 0,
...filters,
sortBy: filters.sortBy || SongListSort.ID,
sortOrder: filters.sortOrder || SortOrder.ASC,
},
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = data?.items.length;
return (
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
@@ -206,7 +198,10 @@ const PlaylistDetailSongListRoute = () => {
</Paper>
</Box>
)}
<PlaylistDetailSongListContent tableRef={tableRef} />
<PlaylistDetailSongListContent
songs={data?.items || []}
tableRef={tableRef}
/>
</AnimatedPage>
);
};
@@ -17,7 +17,7 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
const SERVER_TYPES = [
{ label: 'Jellyfin', value: ServerType.JELLYFIN },
{ label: 'Navidrome', value: ServerType.NAVIDROME },
// { label: 'Subsonic', value: ServerType.SUBSONIC },
{ label: 'Subsonic', value: ServerType.SUBSONIC },
];
interface AddServerFormProps {
@@ -246,28 +246,6 @@ export const ControlSettings = () => {
isHidden: !isElectron(),
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Go to playlist songs page by default"
defaultChecked={settings.defaultFullPlaylist}
onChange={(e) =>
setSettings({
general: {
...settings,
defaultFullPlaylist: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.skipPlaylistPage', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={controlOptions} />;
@@ -326,7 +326,9 @@ export const MpvSettings = () => {
<NumberInput
defaultValue={settings.mpvProperties.replayGainFallbackDB}
width={75}
onBlur={(e) => handleSetMpvProperty('replayGainFallbackDB', e)}
onBlur={(e) =>
handleSetMpvProperty('replayGainFallbackDB', Number(e.currentTarget.value))
}
/>
),
description: t('setting.replayGainFallback', {
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { RiAddBoxFill, RiAddCircleFill, RiPlayFill } from 'react-icons/ri';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { LibraryItem } from '/@/renderer/api/types';
import { LibraryItem, PlaylistListSort, SortOrder } from '/@/renderer/api/types';
import { Button, Text } from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlaylistList } from '/@/renderer/features/playlists';
@@ -14,20 +14,12 @@ import { Play } from '/@/renderer/types';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { useHideScrollbar } from '/@/renderer/hooks';
import { useGeneralSettings } from '/@/renderer/store';
interface SidebarPlaylistListProps {
data: ReturnType<typeof usePlaylistList>['data'];
}
import { useCurrentServer } from '/@/renderer/store';
const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
const { t } = useTranslation();
const path = data?.items[index].id
? data.defaultFullPlaylist
? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id })
: generatePath(AppRoute.PLAYLISTS_DETAIL, {
playlistId: data?.items[index].id,
})
? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id })
: undefined;
return (
@@ -121,10 +113,19 @@ const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
);
};
export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
export const SidebarPlaylistList = () => {
const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0);
const handlePlayQueueAdd = usePlayQueueAdd();
const { defaultFullPlaylist } = useGeneralSettings();
const server = useCurrentServer();
const playlistsQuery = usePlaylistList({
query: {
sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId: server?.id,
});
const [rect, setRect] = useState({
height: 0,
@@ -148,11 +149,10 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
const memoizedItemData = useMemo(() => {
return {
defaultFullPlaylist,
handlePlay: handlePlayPlaylist,
items: data?.items,
items: playlistsQuery?.data?.items,
};
}, [data?.items, defaultFullPlaylist, handlePlayPlaylist]);
}, [playlistsQuery?.data?.items, handlePlayPlaylist]);
return (
<Flex
@@ -168,7 +168,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
: 'overlay-scrollbar'
}
height={debounced.height}
itemCount={data?.items?.length || 0}
itemCount={playlistsQuery?.data?.items?.length || 0}
itemData={memoizedItemData}
itemSize={25}
overscanCount={20}
@@ -1,7 +1,7 @@
import { MouseEvent, useMemo } from 'react';
import { Box, Center, Divider, Group, Stack } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { AnimatePresence, motion } from 'framer-motion';
import { MouseEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiAddFill, RiArrowDownSLine, RiDiscLine, RiListUnordered } from 'react-icons/ri';
import { Link, useLocation } from 'react-router-dom';
@@ -11,9 +11,9 @@ import {
useGeneralSettings,
useWindowSettings,
} from '../../../store/settings.store';
import { PlaylistListSort, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
import { ServerType } from '/@/renderer/api/types';
import { Button, MotionStack, Tooltip } from '/@/renderer/components';
import { CreatePlaylistForm } from '/@/renderer/features/playlists';
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
@@ -110,15 +110,6 @@ export const Sidebar = () => {
});
};
const playlistsQuery = usePlaylistList({
query: {
sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId: server?.id,
});
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const expandFullScreenPlayer = () => {
@@ -198,7 +189,6 @@ export const Sidebar = () => {
>
{t('page.sidebar.playlists', { postProcess: 'titleCase' })}
</Box>
{playlistsQuery.isLoading && <Spinner />}
</Group>
<Group spacing="sm">
<Button
@@ -233,7 +223,7 @@ export const Sidebar = () => {
</Button>
</Group>
</Group>
<SidebarPlaylistList data={playlistsQuery.data} />
<SidebarPlaylistList />
</>
)}
</MotionStack>
@@ -29,6 +29,7 @@ import { queryClient } from '/@/renderer/lib/react-query';
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filters';
const FILTERS = {
jellyfin: [
@@ -160,14 +161,26 @@ const FILTERS = {
value: SongListSort.YEAR,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME,
},
],
};
interface SongListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount: number | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFiltersProps) => {
export const SongListHeaderFilters = ({
gridRef,
tableRef,
itemCount,
}: SongListHeaderFiltersProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const { pageKey, handlePlay, customFilters } = useListContext();
@@ -179,6 +192,7 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
useListStoreActions();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.SONG,
server,
});
@@ -387,25 +401,34 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
};
const handleOpenFiltersModal = () => {
let FilterComponent;
switch (server?.type) {
case ServerType.NAVIDROME:
FilterComponent = NavidromeSongFilters;
break;
case ServerType.JELLYFIN:
FilterComponent = JellyfinSongFilters;
break;
case ServerType.SUBSONIC:
FilterComponent = SubsonicSongFilters;
break;
default:
break;
}
if (!FilterComponent) {
return;
}
openModal({
children: (
<>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeSongFilters
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
) : (
<JellyfinSongFilters
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
)}
</>
<FilterComponent
customFilters={customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
),
title: 'Song Filters',
});
@@ -424,8 +447,17 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
.some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
const isSubsonicFilterApplied =
server?.type === ServerType.SUBSONIC && (filter?.isFavorite || filter?.genre);
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
}, [
filter._custom?.jellyfin,
filter._custom?.navidrome,
filter?.genre,
filter?.isFavorite,
server?.type,
]);
const isFolderFilterApplied = useMemo(() => {
return filter.musicFolderId !== undefined;
@@ -462,11 +494,15 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
/>
{server?.type !== ServerType.SUBSONIC && (
<>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
/>
</>
)}
{server?.type === ServerType.JELLYFIN && (
<>
<Divider orientation="vertical" />
@@ -32,6 +32,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
const cq = useContainerQuery();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.SONG,
server,
});
@@ -94,6 +95,7 @@ export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongList
<FilterBar>
<SongListHeaderFilters
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</FilterBar>
@@ -0,0 +1,109 @@
import { ChangeEvent, useMemo } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { Select, Switch, Text } from '/@/renderer/components';
import { useGenreList } from '/@/renderer/features/genres';
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
import { useTranslation } from 'react-i18next';
interface SubsonicSongFiltersProps {
customFilters?: Partial<SongListFilter>;
onFilterChange: (filters: SongListFilter) => void;
pageKey: string;
serverId?: string;
}
export const SubsonicSongFilters = ({
customFilters,
onFilterChange,
pageKey,
serverId,
}: SubsonicSongFiltersProps) => {
const { t } = useTranslation();
const { setFilter } = useListStoreActions();
const filter = useListFilterByKey({ key: pageKey });
const isGenrePage = customFilters?._custom?.navidrome?.genre_id !== undefined;
const genreListQuery = useGenreList({
query: {
sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId,
});
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
label: genre.name,
value: genre.id,
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilter({
customFilters,
data: {
genre: e || undefined,
},
itemType: LibraryItem.SONG,
key: pageKey,
}) as SongListFilter;
onFilterChange(updatedFilters);
}, 250);
const toggleFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
data: {
isFavorite: e.target.checked,
},
itemType: LibraryItem.SONG,
key: pageKey,
}) as SongListFilter;
onFilterChange(updatedFilters);
},
value: filter.isFavorite,
},
];
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
<Group
key={`ss-filter-${filter.label}`}
position="apart"
>
<Text>{filter.label}</Text>
<Switch
checked={filter?.value || false}
size="xs"
onChange={filter.onChange}
/>
</Group>
))}
<Divider my="0.5rem" />
<Group grow>
{!isGenrePage && (
<Select
clearable
searchable
data={genreList}
defaultValue={filter.genre}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
width={150}
onChange={handleGenresFilter}
/>
)}
</Group>
</Stack>
);
};
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import type { SongListQuery } from '/@/renderer/api/types';
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const getSongListCountQuery = (query: SongListQuery) => {
const filter: Record<string, unknown> = {};
if (query.searchTerm) filter.searchTerm = query.searchTerm;
if (query.genreId) filter.genreId = query.genreId;
if (query.musicFolderId) filter.musicFolderId = query.musicFolderId;
if (query.isFavorite) filter.isFavorite = query.isFavorite;
if (query.genre) filter.genre = query.genre;
if (Object.keys(filter).length === 0) return undefined;
return filter;
};
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getSongListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.songs.count(serverId || '', getSongListCountQuery(query)),
...options,
});
};
@@ -9,11 +9,11 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared';
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
import { useSongList } from '/@/renderer/features/songs/queries/song-list-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
import { titleCase } from '/@/renderer/utils';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useSongListCount } from '/@/renderer/features/songs/queries/song-list-count-query';
const TrackListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
@@ -36,6 +36,8 @@ const TrackListRoute = () => {
genre_id: genreId,
},
},
genre: genreId,
genreId,
}),
};
@@ -74,7 +76,7 @@ const TrackListRoute = () => {
return genre?.name;
}, [genreId, genreList.data]);
const itemCountCheck = useSongList({
const itemCountCheck = useSongListCount({
options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
@@ -87,10 +89,7 @@ const TrackListRoute = () => {
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const handlePlay = useCallback(
async (args: { initialSongId?: string; playType: Play }) => {
+30 -7
View File
@@ -10,14 +10,18 @@ import orderBy from 'lodash/orderBy';
interface UseHandleListFilterChangeProps {
isClientSideSort?: boolean;
itemCount?: number;
itemType: LibraryItem;
server: ServerListItem | null;
}
const BLOCK_SIZE = 500;
export const useListFilterRefresh = ({
server,
itemType,
isClientSideSort,
itemCount,
}: UseHandleListFilterChangeProps) => {
const queryClient = useQueryClient();
@@ -78,7 +82,7 @@ export const useListFilterRefresh = ({
const queryKey = queryKeyFn(server?.id || '', query);
const res = await queryClient.fetchQuery({
const results = (await queryClient.fetchQuery({
queryFn: async ({ signal }) => {
return queryFn({
apiClientProps: {
@@ -89,20 +93,39 @@ export const useListFilterRefresh = ({
});
},
queryKey,
});
})) as BasePaginatedResponse<any>;
if (isClientSideSort && res?.items) {
if (isClientSideSort && results?.items) {
const sortedResults = orderBy(
res.items,
results.items,
[(item) => String(item[filter.sortBy]).toLowerCase()],
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
);
params.successCallback(sortedResults || [], res?.totalRecordCount || 0);
params.successCallback(
sortedResults || [],
results?.totalRecordCount || itemCount,
);
return;
}
params.successCallback(res?.items || [], res?.totalRecordCount || 0);
if (results.totalRecordCount === null) {
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
const lastRowIndex = hasMoreRows
? undefined
: (filter.offset || 0) + results.items.length;
params.successCallback(
results?.items || [],
hasMoreRows ? undefined : lastRowIndex,
);
return;
}
params.successCallback(
results?.items || [],
results?.totalRecordCount || itemCount,
);
},
rowCount: undefined,
@@ -112,7 +135,7 @@ export const useListFilterRefresh = ({
tableRef.current?.api.purgeInfiniteCache();
tableRef.current?.api.ensureIndexVisible(0, 'top');
},
[isClientSideSort, queryClient, queryFn, queryKeyFn, server],
[isClientSideSort, itemCount, queryClient, queryFn, queryKeyFn, server],
);
const handleRefreshGrid = useCallback(
+3 -1
View File
@@ -222,7 +222,9 @@ export const WindowBar = () => {
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
const queueString = length ? `(${index + 1} / ${length}) ` : '';
const title = length
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName}`
? currentSong?.artistName
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName}`
: `${statusString}${queueString}${currentSong?.name}`
: 'Feishin';
document.title = title;
-9
View File
@@ -18,10 +18,6 @@ const AlbumListRoute = lazy(() => import('/@/renderer/features/albums/routes/alb
const SongListRoute = lazy(() => import('/@/renderer/features/songs/routes/song-list-route'));
const PlaylistDetailRoute = lazy(
() => import('/@/renderer/features/playlists/routes/playlist-detail-route'),
);
const PlaylistDetailSongListRoute = lazy(
() => import('/@/renderer/features/playlists/routes/playlist-detail-song-list-route'),
);
@@ -136,11 +132,6 @@ export const AppRouter = () => {
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS}
/>
<Route
element={<PlaylistDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL}
/>
<Route
element={<PlaylistDetailSongListRoute />}
errorElement={<RouteErrorBoundary />}